<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed"/>
    <language>en</language>
    <item>
      <title>Techniques for Making AI Agents Your QA Team for Reliable Modernization</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:27:34 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/techniques-for-making-ai-agents-your-qa-team-for-reliable-modernization-10ho</link>
      <guid>https://dev.to/cobuildx-ai/techniques-for-making-ai-agents-your-qa-team-for-reliable-modernization-10ho</guid>
      <description>&lt;p&gt;&lt;em&gt;Agents can port a legacy screen in minutes, so the bottleneck moves to proving the new code behaves like the old. These are the techniques that turn agents into a QA team you can trust: layered gates, characterization tests, maker-checker review and visual diffs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AI agents can now port a legacy screen in minutes. That moves the bottleneck. The hard question in a legacy system migration is no longer "can we write the new code?" but "how do we know it behaves exactly like the old code?"&lt;/p&gt;

&lt;p&gt;Manual QA can't keep up with agent-speed porting, and a passing build proves very little. The answer is to point agents at verification too, and to set them up so they find problems instead of approving them. Here are the techniques that make an agent QA team you can trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why migration testing is different
&lt;/h2&gt;

&lt;p&gt;Normal QA asks whether software meets a spec. Modernization QA asks whether the new system matches the old one, and the old one usually has no spec. Its real behavior is spread across templates, watchers, global events, server quirks and years of bug fixes nobody wrote down.&lt;/p&gt;

&lt;p&gt;So the goal is &lt;strong&gt;parity&lt;/strong&gt;: same outputs, same side effects, same edge cases, unless a difference is a deliberate, recorded decision. Every technique below serves that goal.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Program testing can be used to show the presence of bugs, but never to show their absence.” — &lt;strong&gt;Edsger W. Dijkstra&lt;/strong&gt;, &lt;a href="https://www.cs.utexas.edu/~EWD/transcriptions/EWD02xx/EWD249.html" rel="noopener noreferrer"&gt;Notes on Structured Programming&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Technique 1: Five layers, cheapest first
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkvdokc5xf9cc5nrjx92j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkvdokc5xf9cc5nrjx92j.png" alt="Five QA layers from cheapest to most expensive: static gates, characterization tests, behavior parity review, visual and end-to-end diff, and human sign-off on a risk-ranked sample" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Don't send every change to the most expensive check. Stack the gates so cheap, deterministic checks catch the obvious problems before an agent or a person spends time on them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Static gates.&lt;/strong&gt; Types, lint, build, and a banned-API list (no jQuery in the React tree, no direct DOM access, no old global stores).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Characterization tests.&lt;/strong&gt; Tests that record what the old code does today, written before the port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior parity review.&lt;/strong&gt; A separate agent compares old and new logic and reports differences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual and end-to-end diff.&lt;/strong&gt; Screenshots, user flows and network calls, old against new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human sign-off.&lt;/strong&gt; People review a risk-ranked sample, not every line.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing merges on the porter's word. Each layer is a gate, and a failure sends the unit back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtmvlv6ypkvb90xs4yh1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtmvlv6ypkvb90xs4yh1.png" alt="Bar chart of the modeled share of behavior bugs reaching production as layers stack: 100% with no gates, 70% after static gates, 28% after characterization tests, 14% after the parity review agent, 8.4% after visual and E2E diff, 5.9% after a human risk sample" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No single layer is close to perfect, and none needs to be. With modest catch rates for each layer, the model above lets through about 6% of behavior bugs, against 28% when you stop at tests. The layers only multiply like this when they look at different things, which is why each one has a distinct job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 2: Write characterization tests before the port
&lt;/h2&gt;

&lt;p&gt;Have a test-writer agent read the &lt;strong&gt;old&lt;/strong&gt; unit and write tests that pin down its current behavior: rendered output for known inputs, events fired, API calls made, validation messages shown, what happens on empty, error and slow responses. These tests run against the old code first and must pass there.&lt;/p&gt;

&lt;p&gt;Then the same tests run against the new code. Because they were written from the original, not from the port, they can't inherit the porter's misunderstandings.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Lock the tests once they pass on the old code. If a porter edits a characterization test to make it pass, that is a finding, not a fix.&lt;/p&gt;

&lt;p&gt;“To me, legacy code is simply code without tests.” — &lt;strong&gt;Michael Feathers&lt;/strong&gt;, &lt;a href="https://www.oreilly.com/library/view/working-effectively-with/0131177052/" rel="noopener noreferrer"&gt;Working Effectively with Legacy Code&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Technique 3: Maker and checker never share a context
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdpuqrnkcx0iorqnvnge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdpuqrnkcx0iorqnvnge.png" alt="The porter agent writes new code and sends the diff and test results to a reviewer agent that sees only old code, new code and tests. The reviewer returns findings with file, line and scenario, looping until there are no confirmed findings or a human breaks the tie" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An agent reviewing its own work mostly agrees with itself. It already has a story for why every line is right. The reviewer has to be a separate agent with a fresh context that sees only the evidence: the original code, the new code and the test output.&lt;/p&gt;

&lt;p&gt;Give the reviewer a narrow job and a strict output format. Ask for concrete findings, each with a file, a line and a failing scenario, and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compare src/legacy/orders/OrderList.js with src/app/orders/OrderList.tsx.
Report only behavior differences a user or API consumer could observe.
For each: file:line, the input or scenario, old result, new result.
Do not comment on style. Do not suggest refactors.
If you find none, say so.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run a second pass on the findings themselves. A verifier agent tries to reproduce each one, and only confirmed findings go back to the porter. This keeps false alarms from wasting porter time and teaching the team to ignore the reviewer.&lt;/p&gt;

&lt;p&gt;The same logic applies to choosing models for each layer. Use a different model family for the reviewer than for the code it checks, put cheap fast models on high-volume checks, and keep top-tier models for the judgment calls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4nd12c037fvi9yq5cim.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4nd12c037fvi9yq5cim.png" alt="Bar chart of an example setup, agent calls per migrated unit by QA layer: static gates use no model, characterization tests 1 call on Sonnet 5.5, behavior parity review 1.3 calls on GPT-6 Astra, visual and E2E diff 2 calls on Sonnet 5.5, finding verifier 1.5 calls on Haiku 5.5, human sign-off prep 0.2 calls on Opus 5.5" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 4: Diff what the user sees and what the server sees
&lt;/h2&gt;

&lt;p&gt;Logic review misses things a user notices immediately: a shifted layout, a missing loading spinner, a date in the wrong format. An agent driving a browser can run the same flow on the old and new app, take screenshots at each step and compare them.&lt;/p&gt;

&lt;p&gt;Record network traffic too. If the old screen made three API calls with certain parameters and the new one makes five, or drops a header, something changed. Network diffs catch problems that never show up on screen until production traffic hits them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 5: Spend human time where risk is highest
&lt;/h2&gt;

&lt;p&gt;Agents make human review cheaper, not unnecessary. Have the orchestrator rank units by risk: payments, auth, permissions, data writes and anything with a history of incidents go first. People review those in full, and a sample of the rest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr21rezcvtn6sm4seowu4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr21rezcvtn6sm4seowu4.png" alt="Grouped bar chart of modeled human review hours for 200 units. Reviewing every unit takes 400 hours. Risk-ranked review takes 124 hours: 80 for all high-risk units, 24 for a 20% sample of medium, 20 for a 10% sample of low" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this model, risk ranking cuts senior review time by about two thirds while still reviewing every high-risk unit in full. Agents run the first four layers on every unit regardless.&lt;/p&gt;

&lt;p&gt;Every deliberate difference, such as a bug you chose to fix instead of copying, goes into a decisions log with a reason. When parity tests fail later, the log tells the reviewer which differences are expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 6: Measure the QA team itself
&lt;/h2&gt;

&lt;p&gt;An agent QA team needs QA too. Seed a few known bugs into ported units, such as a dropped validation rule, a changed sort order or a missing error state, and check that the layers catch them. If a seeded bug slips through, you know which layer to strengthen before a real one does.&lt;/p&gt;

&lt;p&gt;Track the reviewer's precision as well: the share of its findings that the verifier confirms. A reviewer that cries wolf gets ignored, and a reviewer that never finds anything is probably not looking hard enough. Both numbers belong on the project dashboard next to units migrated.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seeded-bug catch rate:&lt;/strong&gt; which layer caught each planted defect, and which missed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewer precision:&lt;/strong&gt; confirmed findings divided by all findings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escaped defects:&lt;/strong&gt; bugs found after merge, traced back to the layer that should have caught them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this looks like in MigrateX
&lt;/h2&gt;

&lt;p&gt;Our MigrateX plugins build this in. Each framework plugin has a read-only &lt;strong&gt;parity-reviewer&lt;/strong&gt; agent that checks one migrated piece against its original for behavior, markup, data and timing differences before the old code is removed. It can't edit code, so it can only report. The porter fixes, the reviewer checks again, and an engineer signs off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Modernization QA is about parity with an undocumented system, not meeting a spec.&lt;/li&gt;
&lt;li&gt;Stack gates from cheap to expensive: static checks, characterization tests, parity review, visual and network diffs, human sign-off.&lt;/li&gt;
&lt;li&gt;Write tests from the old code before the port, and lock them.&lt;/li&gt;
&lt;li&gt;Keep the reviewer in a separate context, ask for concrete findings, and verify findings before acting on them.&lt;/li&gt;
&lt;li&gt;Spend human review on the riskiest units, and log every deliberate difference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Related reading:&lt;/strong&gt; &lt;a href="https://cobuildx.ai/blog/multi-agent-modernization" rel="noopener noreferrer"&gt;how multi-agent AI speeds up application migration&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/modernization-mistakes-to-avoid" rel="noopener noreferrer"&gt;10 legacy modernization mistakes to avoid&lt;/a&gt; and &lt;a href="https://cobuildx.ai/blog/software-migration-lessons-shopify" rel="noopener noreferrer"&gt;software migration lessons from Shopify's AI rewrite&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/ai-agents-qa-team-modernization" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>migration</category>
      <category>agents</category>
    </item>
    <item>
      <title>10x Your Modernization Efforts With Multi-Agent Teams</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:26:29 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/10x-your-modernization-efforts-with-multi-agent-teams-16pd</link>
      <guid>https://dev.to/cobuildx-ai/10x-your-modernization-efforts-with-multi-agent-teams-16pd</guid>
      <description>&lt;p&gt;&lt;em&gt;One agent porting one file at a time stalls fast. Teams getting big gains split the work across an orchestrator, parallel porters, a test writer and an independent reviewer. Here is how that setup works and where the speed-up really comes from.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most teams try AI on an application migration the same way: one chat, one agent, one file at a time. It works for a demo. Then the codebase turns out to have 400 components, a router nobody fully understands and a service layer with side effects in every getter, and the single agent becomes the bottleneck. It forgets what it ported yesterday, repeats the same mistakes, and marks its own homework.&lt;/p&gt;

&lt;p&gt;The teams getting large gains don't use a smarter agent. They use &lt;strong&gt;more agents with narrower jobs&lt;/strong&gt;, coordinated around a plan. This post covers how that setup works, where the speed-up really comes from, and what has to be true before you get anything close to 10x.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one agent stalls
&lt;/h2&gt;

&lt;p&gt;A single agent doing a migration carries the whole project in one context window: the old code, the new code, the conventions, the test output and its own earlier decisions. As the window fills, quality drops. It starts to drift from conventions it set itself, and it re-reads files it already understood.&lt;/p&gt;

&lt;p&gt;It also works in series. Port, test, fix, review, then the next unit. Even if each step is fast, the project's speed is the sum of every step.&lt;/p&gt;

&lt;p&gt;And when the same agent that wrote the code reviews it, it mostly agrees with itself. The bugs that matter most in a migration, small behavior differences between old and new, are exactly the ones its own reasoning has already explained away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multi-agent setup for application migration
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ud8236sva3equebxuis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ud8236sva3equebxuis.png" alt="An orchestrator holding the unit graph and work queue sends work to an inventory agent, two porter agents and a test-writer agent. All of them hand results to an independent parity reviewer, and anything that fails goes back to the queue" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A working setup has five roles. Each is an agent with its own context, its own instructions and a narrow set of tools.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orchestrator.&lt;/strong&gt; Owns the plan: the list of units, their dependencies and their status. It hands out work and never writes app code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inventory agent.&lt;/strong&gt; Read-only. Maps every route, component, service, global and hidden wiring point before anyone writes a line. Its output is the unit graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Porters.&lt;/strong&gt; Each takes one unit at a time and moves it to the target stack, following a shared conventions file. Several run at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test writer.&lt;/strong&gt; Captures what the old unit does today, as characterization tests, before or alongside the port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parity reviewer.&lt;/strong&gt; A fresh agent that sees only the old code, the new code and the test results, and reports concrete differences with a file, line and failing scenario.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Split by &lt;strong&gt;unit&lt;/strong&gt;, not by step. Give a porter a whole component with its tests, not "all the templates" across the app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where the speed-up actually comes from
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fitel7gjvphuwyg2v7p38.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fitel7gjvphuwyg2v7p38.png" alt="Line chart of modeled speed-up against the number of porter agents, using Amdahl's law. With 95% of units independent, 16 porters give about 9.1 times; with 80% about 4.0 times; with 50% about 1.9 times" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The chart above is a simple model (Amdahl's law), but the shape holds on real projects. Parallel porters only help when the units they work on don't depend on each other. If unit 6 needs unit 2's new API, it waits. So the real driver of throughput is the &lt;strong&gt;unit graph&lt;/strong&gt;: how well the inventory step breaks the app into pieces that can move independently.&lt;/p&gt;

&lt;p&gt;The other multiplier is less obvious: &lt;strong&gt;fewer re-dos&lt;/strong&gt;. Each agent's context stays small and focused, so the work is more consistent. The reviewer catches parity bugs while the unit is still fresh, not at user testing three months later. In practice, avoided rework is often worth more than raw parallelism.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foj2ep68129m6eu41pl7g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foj2ep68129m6eu41pl7g.png" alt="Bar chart of modeled elapsed hours to migrate 120 units: one agent 180 hours, 4 porters and 1 reviewer 84 hours, 8 porters and 1 reviewer still 84 hours because review is the bottleneck, 4 or 8 porters with 2 reviewers 60 hours, 8 porters and 4 reviewers 48 hours" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Review capacity matters just as much. In the model above, doubling porters from four to eight with a single reviewer saves nothing: the reviewer is the bottleneck. Adding reviewers is what moves the number.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Adding manpower to a late software project makes it later.” — &lt;strong&gt;Fred Brooks&lt;/strong&gt;, &lt;a href="https://en.wikipedia.org/wiki/The_Mythical_Man-Month" rel="noopener noreferrer"&gt;The Mythical Man-Month&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Put together, a well-cut graph, several porters and an early independent review can turn a project measured in quarters into one measured in weeks. A badly cut graph with ten agents is just ten agents waiting on each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five rules that make it work
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;“There is nothing so useless as doing efficiently that which should not be done at all.” — &lt;strong&gt;&lt;a href="https://en.wikipedia.org/wiki/Peter_Drucker" rel="noopener noreferrer"&gt;Peter Drucker&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inventory before you port.&lt;/strong&gt; No porter starts until the unit graph exists. The graph decides the order, the parallelism and what "done" means.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One shared conventions file.&lt;/strong&gt; Naming, state management, styling, error handling, folder layout. Every porter reads it, and the reviewer checks against it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small, verifiable units.&lt;/strong&gt; A unit should be portable and reviewable on its own: one route, one component tree, one service. If a reviewer can't judge it in one pass, it's too big.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maker and checker never share a context.&lt;/strong&gt; The reviewer gets no access to the porter's reasoning, only the evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable state outside the agents.&lt;/strong&gt; Unit status, decisions and open findings live in files in the repo, not in any agent's memory. Any agent can be restarted without losing the project.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Which model for which agent
&lt;/h2&gt;

&lt;p&gt;Running one flagship model for every role is the simplest setup, and the most wasteful. Roles need different things. The orchestrator and the inventory agent make the decisions everything else depends on, so they get the deepest reasoning: &lt;strong&gt;Fable 5.1&lt;/strong&gt; or &lt;strong&gt;Opus 5.5&lt;/strong&gt;. Porters and the test writer do high-volume, well-specified work against a conventions file, which is what &lt;strong&gt;Sonnet 5.5&lt;/strong&gt; is good at. Checking whether a finding reproduces is a narrow yes-or-no task for a fast model like &lt;strong&gt;Haiku 5.5&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmiiruvc4sd0xdiymzym2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmiiruvc4sd0xdiymzym2.png" alt="Bar chart of an example setup, agent calls per migrated unit by role: orchestrator 1 call on Fable 5.1, inventory 0.17 calls on Opus 5.5, porters 3 calls on Sonnet 5.5, test writer 1 call on Sonnet 5.5, parity reviewer 1.3 calls on GPT-6 Astra, finding verifier 1.5 calls on Haiku 5.5, each with an alternative model" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reviewer is the one place to deliberately switch model families. If Claude models write the code, a reviewer such as OpenAI's &lt;strong&gt;GPT-6 Astra&lt;/strong&gt; is less likely to share their blind spots, and the reverse holds too. Treat the chart as a starting point, not a rule. Swap models per role as you measure what each one gets right on your codebase.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F438jrkavsra4nl1qubr6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F438jrkavsra4nl1qubr6.png" alt="Modeled share of agent calls for a 120-unit migration. One flagship for every role: Fable 5.1 handles 100% of calls. Tiered by role: Fable 5.1 13%, Opus 5.5 2%, GPT-6 Astra 16%, Sonnet 5.5 50%, Haiku 5.5 19%" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this model, tiering moves about 70% of calls to Sonnet 5.5 and Haiku 5.5, while every decision that shapes the project still goes through a top-tier model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it goes wrong
&lt;/h2&gt;

&lt;p&gt;Two porters editing the same shared file is the most common failure. Give shared modules (the router, the store, the design system) a single owner, and have porters ask for changes through the orchestrator.&lt;/p&gt;

&lt;p&gt;The second is review becoming the bottleneck. If four porters each produce a unit an hour and the reviewer takes two hours per unit, you've built a queue, not a pipeline. Run more than one reviewer, or keep units small enough to review quickly.&lt;/p&gt;

&lt;p&gt;The third is trusting green checks. A porter can make tests pass by changing the tests. Lock characterization tests once they're written, and treat any change to them as a finding in its own right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a pilot
&lt;/h2&gt;

&lt;p&gt;Don't switch a whole program to multi-agent on day one. Pick one slice of the app, ten to twenty units with a mix of easy and awkward ones, and run the full setup on it: inventory, conventions file, porters, test writer and reviewer.&lt;/p&gt;

&lt;p&gt;Track three numbers during the pilot: how many units pass review first time, how many reviewer findings turn out to be real, and how long units wait in the review queue. The first tells you whether your conventions and unit sizes work. The second tells you whether to trust the reviewer. The third tells you how many reviewers to run before you add porters.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Scale porters only after first-time pass rate and review wait time look healthy. Otherwise you are just making a longer queue.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How we run it
&lt;/h2&gt;

&lt;p&gt;This is the structure behind &lt;strong&gt;MigrateX&lt;/strong&gt;, our migration toolkit for moving Angular, AngularJS, Backbone and Ember apps to React. Each plugin runs a read-only inventory agent first, writes the plan and unit map into a &lt;code&gt;.migration/&lt;/code&gt; folder in the repo, then ports unit by unit with a separate parity-reviewer agent checking each one against its original before the old code is removed.&lt;/p&gt;

&lt;p&gt;The agents do the volume. Engineers own the plan, the conventions and the final call on anything the reviewer flags. That split, more than any single model, is where the multiplier comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One agent doing everything stalls on context, works in series and grades its own work.&lt;/li&gt;
&lt;li&gt;Use an orchestrator, an inventory agent, parallel porters, a test writer and an independent reviewer.&lt;/li&gt;
&lt;li&gt;The speed-up depends on the unit graph and on avoided rework, not on how many agents you run.&lt;/li&gt;
&lt;li&gt;Keep project state in the repo, give shared files one owner, and lock tests once they're written.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Related reading:&lt;/strong&gt; &lt;a href="https://cobuildx.ai/blog/ai-agents-qa-team-modernization" rel="noopener noreferrer"&gt;a migration testing strategy with AI agents&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/modernization-mistakes-to-avoid" rel="noopener noreferrer"&gt;10 legacy modernization mistakes to avoid&lt;/a&gt; and &lt;a href="https://cobuildx.ai/blog/software-migration-lessons-shopify" rel="noopener noreferrer"&gt;software migration lessons from Shopify's AI rewrite&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/multi-agent-modernization" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>migration</category>
      <category>legacy</category>
    </item>
    <item>
      <title>Build a Tiny M3U Playlist Linter in Python (Zero Dependencies)</title>
      <dc:creator>Marcus Hale</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:26:20 +0000</pubDate>
      <link>https://dev.to/new_iptv_ce70d4682c7684d3/build-a-tiny-m3u-playlist-linter-in-python-zero-dependencies-3g15</link>
      <guid>https://dev.to/new_iptv_ce70d4682c7684d3/build-a-tiny-m3u-playlist-linter-in-python-zero-dependencies-3g15</guid>
      <description>&lt;p&gt;M3U files look simple. They're plain text: a header, then pairs of lines where one describes a stream and the next one points to it. Because they're so easy to edit by hand, they're also easy to break. One missing URL line shifts every entry below it, a duplicated link hides in line 4,000, and a typo like &lt;code&gt;htps://&lt;/code&gt; fails silently in most players.&lt;/p&gt;

&lt;p&gt;In this post we'll build &lt;code&gt;m3u_check.py&lt;/code&gt;, a small command-line linter that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validates the basic M3U structure&lt;/li&gt;
&lt;li&gt;reports entries with no URL (and URLs with no entry)&lt;/li&gt;
&lt;li&gt;flags duplicate URLs and duplicate names&lt;/li&gt;
&lt;li&gt;warns when &lt;code&gt;tvg-id&lt;/code&gt; is missing, so you know EPG data won't match&lt;/li&gt;
&lt;li&gt;optionally checks every URL with a concurrent &lt;code&gt;HEAD&lt;/code&gt; request&lt;/li&gt;
&lt;li&gt;returns a non-zero exit code so you can drop it into CI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No &lt;code&gt;pip install&lt;/code&gt; needed. Everything comes from Python's standard library.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 30-second refresher on the format
&lt;/h2&gt;

&lt;p&gt;An extended M3U playlist looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#EXTM3U
#EXTINF:-1 tvg-id="news.one" tvg-logo="https://example.com/n1.png" group-title="News",News One
https://example.com/live/news1.m3u8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#EXTM3U&lt;/code&gt; must be on the first line.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#EXTINF:&lt;/code&gt; starts an entry. &lt;code&gt;-1&lt;/code&gt; is the duration (&lt;code&gt;-1&lt;/code&gt; means a live stream with no fixed length).&lt;/li&gt;
&lt;li&gt;Everything between the duration and the &lt;strong&gt;last comma&lt;/strong&gt; is a set of &lt;code&gt;key="value"&lt;/code&gt; attributes. &lt;code&gt;tvg-id&lt;/code&gt; links the entry to an EPG (TV guide) source, &lt;code&gt;tvg-logo&lt;/code&gt; holds an icon, and &lt;code&gt;group-title&lt;/code&gt; sets the category.&lt;/li&gt;
&lt;li&gt;After the comma comes the display name.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;next non-comment line&lt;/strong&gt; is the URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That "next line" rule is where most bugs come from, so it's the core of our parser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broken playlist we'll test with
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;sample.m3u&lt;/code&gt;. It hides four problems. Try to spot them before reading on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#EXTM3U
#EXTINF:-1 tvg-id="news.one" tvg-logo="https://example.com/n1.png" group-title="News",News One
https://example.com/live/news1.m3u8
#EXTINF:-1 tvg-id="" group-title="Sports",Sports Two
https://example.com/live/sports2.m3u8
#EXTINF:-1 tvg-id="movies.three" group-title="Movies",Movies Three
https://example.com/live/news1.m3u8
#EXTINF:-1 tvg-id="docs.four" group-title="Docs",Docs Four
#EXTINF:-1 tvg-id="kids.five" group-title="Kids",Kids Five
htps://example.com/live/kids5.m3u8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Parse entries with a tiny state machine
&lt;/h2&gt;

&lt;p&gt;We read the file line by line and keep one variable, &lt;code&gt;pending&lt;/code&gt;, which holds the &lt;code&gt;#EXTINF&lt;/code&gt; entry that is still waiting for its URL.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If we see &lt;code&gt;#EXTINF&lt;/code&gt; while something is already pending, the previous entry never got a URL.&lt;/li&gt;
&lt;li&gt;If we see a URL with nothing pending, it's an orphan.&lt;/li&gt;
&lt;li&gt;Otherwise we attach the URL and store the finished entry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Attributes are pulled out with one regex: &lt;code&gt;([\w-]+)="([^"]*)"&lt;/code&gt;. It matches &lt;code&gt;tvg-id="news.one"&lt;/code&gt;, &lt;code&gt;group-title="News"&lt;/code&gt; and so on, and &lt;code&gt;dict()&lt;/code&gt; turns the matches into a lookup table.&lt;/p&gt;

&lt;p&gt;Two small details make the parser more forgiving in real life:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;encoding="utf-8-sig"&lt;/code&gt; silently strips the BOM that some Windows editors add, which would otherwise break the &lt;code&gt;#EXTM3U&lt;/code&gt; check.&lt;/li&gt;
&lt;li&gt;We &lt;code&gt;strip()&lt;/code&gt; every line, so stray &lt;code&gt;\r&lt;/code&gt; characters and trailing spaces don't create fake URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Lint the parsed entries
&lt;/h2&gt;

&lt;p&gt;Once we have clean entries, the checks are short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;URL scheme.&lt;/strong&gt; &lt;code&gt;urllib.parse.urlparse&lt;/code&gt; gives us the scheme. Anything other than &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;https&lt;/code&gt;, &lt;code&gt;rtmp&lt;/code&gt;, &lt;code&gt;rtsp&lt;/code&gt; or &lt;code&gt;udp&lt;/code&gt; is suspicious, and that's how we catch the &lt;code&gt;htps://&lt;/code&gt; typo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing &lt;code&gt;tvg-id&lt;/code&gt;.&lt;/strong&gt; The entry still plays, but players can't match it to guide data, so it shows "No information".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicates.&lt;/strong&gt; &lt;code&gt;collections.Counter&lt;/code&gt; over URLs and names finds them in one pass, even in very large files.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Probe URLs concurrently (optional)
&lt;/h2&gt;

&lt;p&gt;Checking 2,000 URLs one by one with a 5-second timeout could take hours. &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; lets us send many &lt;code&gt;HEAD&lt;/code&gt; requests in parallel. Network calls spend most of their time waiting, so threads work well here despite the GIL.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;HEAD&lt;/code&gt; instead of &lt;code&gt;GET&lt;/code&gt; because we only want the status code, not the video data. A few servers reject &lt;code&gt;HEAD&lt;/code&gt; with a &lt;code&gt;405&lt;/code&gt;. If you see a lot of those, switching the probe to a &lt;code&gt;GET&lt;/code&gt; that reads only the first few bytes is a simple fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;m3u_check.py - lint an M3U/M3U8 playlist and (optionally) probe its URLs.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlopen&lt;/span&gt;

&lt;span class="n"&gt;ATTR_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;([\w-]+)=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;([^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]*)&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return (entries, problems). Each entry is a dict with line, name, attrs, url.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8-sig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#EXTM3U&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing #EXTM3U header on line 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#EXTINF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c1"&gt;# blank lines, header and other directives
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#EXTINF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; has no URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(no name)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attrs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ATTR_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# a URL line
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;URL without an #EXTINF line above it&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(no name)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attrs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}}&lt;/span&gt;
            &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;
            &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; has no URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rtmp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rtsp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;udp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; has an odd URL: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attrs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tvg-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; has no tvg-id (EPG will not match)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate URL x&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;duplicate name x&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skipped (not http)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HEAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;m3u-check/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# timeouts, DNS errors, 4xx/5xx
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__class__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lint an M3U playlist.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;playlist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--check-urls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;store_true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;send a HEAD request to each http(s) URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--workers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;playlist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attrs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group-title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(none)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; entries in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; groups&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; problem(s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  line &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;URL check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 m3u_check.py sample.m3u
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4 entries in 4 groups
  News: 1
  Sports: 1
  Movies: 1
  Kids: 1

4 problem(s)
  duplicate URL x2: https://example.com/live/news1.m3u8
  line 4: "Sports Two" has no tvg-id (EPG will not match)
  line 8: "Docs Four" has no URL
  line 9: "Kids Five" has an odd URL: htps://example.com/live/kids5.m3u8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four hidden problems are caught. Notice that "Docs Four" is reported instead of silently swallowing the next entry's URL, which is exactly what many players do.&lt;/p&gt;

&lt;p&gt;To also test the links:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 m3u_check.py my_playlist.m3u &lt;span class="nt"&gt;--check-urls&lt;/span&gt; &lt;span class="nt"&gt;--workers&lt;/span&gt; 20 &lt;span class="nt"&gt;--timeout&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using it in CI
&lt;/h2&gt;

&lt;p&gt;The script exits with &lt;code&gt;1&lt;/code&gt; when it finds a problem, so it works as a guard in any pipeline. A minimal GitHub Actions step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint playlists&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;for f in playlists/*.m3u; do&lt;/span&gt;
      &lt;span class="s"&gt;python3 m3u_check.py "$f"&lt;/span&gt;
    &lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave &lt;code&gt;--check-urls&lt;/code&gt; out of CI unless you control the servers. Remote streams go up and down for reasons unrelated to your change, and flaky builds teach people to ignore failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ideas to extend it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--fix&lt;/code&gt; mode:&lt;/strong&gt; drop duplicates and orphan lines, then write a cleaned file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EPG cross-check:&lt;/strong&gt; load an XMLTV guide and report &lt;code&gt;tvg-id&lt;/code&gt; values that don't exist in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON output:&lt;/strong&gt; add &lt;code&gt;--json&lt;/code&gt; so other tools can consume the report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group filter:&lt;/strong&gt; &lt;code&gt;--group Sports&lt;/code&gt; to check only one category.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;M3U is one of those formats that's "just text" until it isn't. A hundred lines of standard-library Python turns silent playback failures into clear, line-numbered errors you can fix in seconds. A state machine for parsing, &lt;code&gt;Counter&lt;/code&gt; for duplicates and a thread pool for I/O are patterns you'll reuse well beyond playlists.&lt;/p&gt;

&lt;p&gt;What would you add to it? Let me know in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was written with AI assistance. The code was run and the output above was verified before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>cli</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Prompting Image Models for AI Headshots That Keep Your Likeness</title>
      <dc:creator>Mona Lisa Studio</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:25:40 +0000</pubDate>
      <link>https://dev.to/monalisastudio/prompting-image-models-for-ai-headshots-that-keep-your-likeness-5gil</link>
      <guid>https://dev.to/monalisastudio/prompting-image-models-for-ai-headshots-that-keep-your-likeness-5gil</guid>
      <description>&lt;p&gt;If you have tried turning a casual photo into a professional headshot with an image-editing model such as Gemini's image model (often called Nano Banana), you have probably seen the failure mode: the result looks polished, but it no longer quite looks like the person. The jaw is a little narrower, the skin is airbrushed, the hairline is fuller, and the person looks five years younger.&lt;/p&gt;

&lt;p&gt;This post is a practical guide to avoiding that. It covers how to structure the prompt, which constraints do the most to preserve facial features, what to check in the input photo, and a simple checklist for evaluating the output. Nothing here depends on a specific tool; it applies to any model that takes a reference image plus a text instruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why likeness drifts
&lt;/h2&gt;

&lt;p&gt;An image-to-image model treats your photo as a reference, not as a fixed asset. Everything in the frame is open to reinterpretation unless the prompt says otherwise. For headshots, the model's idea of "professional" tends to lean toward retouched studio photography, so small edits to the face are a common side effect of asking for a "professional headshot".&lt;/p&gt;

&lt;p&gt;The common drift patterns are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skin smoothing&lt;/strong&gt;: pores, freckles and fine lines disappear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Face reshaping&lt;/strong&gt;: slimmer jaw, larger eyes, more symmetrical features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;De-ageing&lt;/strong&gt;: softened under-eye lines and wrinkles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skin tone shifts&lt;/strong&gt;: brighter studio lighting quietly lightens the skin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hairline tidying&lt;/strong&gt;: receding or uneven hairlines get "fixed".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each change on its own can be subtle. Together, they produce someone who looks like a relative.&lt;/p&gt;

&lt;h2&gt;
  
  
  A prompt structure that works
&lt;/h2&gt;

&lt;p&gt;A useful pattern is to split the prompt into four blocks, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reference role&lt;/strong&gt;: tell the model what the image is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve&lt;/strong&gt;: list what must not change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change&lt;/strong&gt;: list what should change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prohibit&lt;/strong&gt;: explicit negatives for the drift patterns above.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Putting the preserve block before the style words matters. If the prompt opens with "professional studio headshot, flawless lighting", the model has already been pulled toward the retouched look before it reads your constraints.&lt;/p&gt;

&lt;p&gt;Here is a template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Reference]
The uploaded photo is the reference and shows the person to portray.

[Preserve]
Keep the person exactly recognizable: face shape, features, eye shape,
age, skin tone and hairline stay as in the photo. Keep real skin texture
and pores. The number of people stays the same.

[Change]
Clothing becomes a navy blazer over a white shirt. Background becomes a
softly blurred light-grey wall. Head-and-shoulders framing, facing the
camera, even soft studio light, crisp focus on the eyes, natural
friendly expression.

[Prohibit]
Do not smooth the skin, slim the face, lighten or change the skin tone,
or make the person look younger or older. Do not add text, logos or
watermarks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bracketed labels are just for readability; you can remove them before sending.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constraints that do the most work
&lt;/h2&gt;

&lt;p&gt;In practice, a few specific instructions carry most of the weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name features, not just "the face".&lt;/strong&gt; "Keep the face the same" is vague. "Face shape, eye shape, nose, jawline, hairline" gives the model concrete targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Say "real skin texture and pores".&lt;/strong&gt; Without it, smoothing is the default for anything described as professional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule out skin tone changes explicitly.&lt;/strong&gt; Lighting instructions can shift skin tone as a side effect. A direct "do not lighten or change the skin tone" line counters that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin the age.&lt;/strong&gt; "Do not make the person look younger or older" stops gradual de-ageing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add personal specifics.&lt;/strong&gt; If someone has a scar, a mole, freckles or a gap in their teeth, name it: "keep the small scar above the left eyebrow". Specific constraints are followed more reliably than general ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change one thing per pass when iterating.&lt;/strong&gt; If the first result needs a different jacket, ask only for the jacket in the next edit, and use the previous output as the new reference. Stacking several changes in one follow-up prompt invites drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see a full working example of this structure, the &lt;a href="https://mona-lisa-img.com/styles/professional-headshot" rel="noopener noreferrer"&gt;professional headshot prompt on Mona Lisa Studio&lt;/a&gt; is published on the page, including its preserve and no-smoothing lines, and is a reasonable starting point to adapt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input photo checklist
&lt;/h2&gt;

&lt;p&gt;The prompt protects likeness, but the photo supplies it. Before generating, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Sharp focus on the face&lt;/strong&gt;, especially the eyes.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Front-facing or slightly turned&lt;/strong&gt;, looking at the lens.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Head and shoulders in frame&lt;/strong&gt;, so the model does not have to invent your build or where the new outfit sits.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Even daylight&lt;/strong&gt;, such as a window to one side. Avoid harsh overhead light that hides eye shape.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;No beauty filters.&lt;/strong&gt; A filtered photo has already altered the face before the model sees it.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;One person only&lt;/strong&gt;, cropped if necessary, so faces do not blend.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Recent photo&lt;/strong&gt;, so the result matches how you look now.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Evaluation checklist
&lt;/h2&gt;

&lt;p&gt;Do not judge the output at thumbnail size. Open it full screen next to the original and go through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Eye spacing and eye shape match.&lt;/li&gt;
&lt;li&gt;[ ] Nose and jawline match.&lt;/li&gt;
&lt;li&gt;[ ] Hairline and hair colour match.&lt;/li&gt;
&lt;li&gt;[ ] Skin texture is still visible; it is not plastic.&lt;/li&gt;
&lt;li&gt;[ ] Skin tone matches the original under comparable light.&lt;/li&gt;
&lt;li&gt;[ ] Apparent age is unchanged.&lt;/li&gt;
&lt;li&gt;[ ] Personal details (scars, moles, freckles) are still there.&lt;/li&gt;
&lt;li&gt;[ ] Hands, if visible, have five natural fingers.&lt;/li&gt;
&lt;li&gt;[ ] No stray text, logos or artifacts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A quick sanity test: show the result to someone who knows the person, without explanation, and ask whether anything looks off. If something has drifted, add a targeted constraint for that feature and regenerate, rather than rewriting the whole prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope and ethics
&lt;/h2&gt;

&lt;p&gt;AI headshots are fine for professional profiles, resumes and team pages. They are not suitable for passports, visas or official ID, which usually require unedited photos. Changing clothing and background is a reasonable presentation choice; changing someone's face is a misrepresentation. And only generate headshots of yourself, or of people who have explicitly agreed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Structure the prompt as reference, preserve, change, prohibit.&lt;/li&gt;
&lt;li&gt;Name specific features and rule out smoothing, reshaping, de-ageing and skin tone shifts.&lt;/li&gt;
&lt;li&gt;Start from a sharp, unfiltered, front-facing photo with shoulders in frame.&lt;/li&gt;
&lt;li&gt;Iterate one change at a time.&lt;/li&gt;
&lt;li&gt;Evaluate at full size against the original.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I'm part of the Mona Lisa Studio team; this post was drafted with AI assistance and edited by me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>promptengineering</category>
      <category>imagegeneration</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Diffing two PDFs in the browser: PDFs don't have lines, so you have to rebuild them</title>
      <dc:creator>Kevin Carne</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:25:36 +0000</pubDate>
      <link>https://dev.to/kevin__a1ead09c07b/diffing-two-pdfs-in-the-browser-pdfs-dont-have-lines-so-you-have-to-rebuild-them-1h2j</link>
      <guid>https://dev.to/kevin__a1ead09c07b/diffing-two-pdfs-in-the-browser-pdfs-dont-have-lines-so-you-have-to-rebuild-them-1h2j</guid>
      <description>&lt;p&gt;``A common request for a PDF tool: "they sent the contract back with a few small changes, what actually changed?" I wanted to answer that without uploading anyone's contract to a server, so the comparison runs entirely in the browser tab.&lt;/p&gt;

&lt;p&gt;The diff part is easy: &lt;a href="https://github.com/kpdecker/jsdiff" rel="noopener noreferrer"&gt;jsdiff&lt;/a&gt; does it in one call. The hard part is getting &lt;em&gt;text you can diff&lt;/em&gt; out of a PDF, because a PDF has no idea what a "line" is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a PDF actually stores
&lt;/h2&gt;

&lt;p&gt;A PDF page isn't a document with paragraphs. It's a list of drawing instructions: "move to (72, 700), draw these glyphs, move to (210, 700), draw these glyphs". Text can be drawn in any order, split anywhere, and positioned anywhere.&lt;/p&gt;

&lt;p&gt;pdf.js gives you that as &lt;code&gt;getTextContent()&lt;/code&gt;: a flat list of text items, each with a string and a transform matrix. The last two values of the matrix, &lt;code&gt;transform[4]&lt;/code&gt; and &lt;code&gt;transform[5]&lt;/code&gt;, are the item's x and y position on the page.&lt;/p&gt;

&lt;p&gt;So a single visual line like&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`plaintext&lt;br&gt;
Notice period: 30 days&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;might come back as three items (&lt;code&gt;"Notice period:"&lt;/code&gt;, &lt;code&gt;"30"&lt;/code&gt;, &lt;code&gt;"days"&lt;/code&gt;), all with the same y.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuilding lines
&lt;/h2&gt;

&lt;p&gt;My first version groups items by their (rounded) y coordinate, then sorts the groups top to bottom. PDF coordinates start at the bottom-left, so "top to bottom" means sorting y &lt;em&gt;descending&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;ts&lt;br&gt;
export async function getPageLines(pdf: PDFDocumentProxy, pageNumber: number): Promise {&lt;br&gt;
  const page = await pdf.getPage(pageNumber);&lt;br&gt;
  const content = await page.getTextContent();&lt;/p&gt;

&lt;p&gt;const lines = new Map();&lt;br&gt;
  for (const item of content.items) {&lt;br&gt;
    if (!("str" in item)) continue; // skip marked-content items&lt;br&gt;
    const y = Math.round(item.transform[5]);&lt;br&gt;
    const bucket = lines.get(y) ?? [];&lt;br&gt;
    bucket.push(item.str);&lt;br&gt;
    lines.set(y, bucket);&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;const sortedYs = Array.from(lines.keys()).sort((a, b) =&amp;gt; b - a);&lt;br&gt;
  return sortedYs&lt;br&gt;
    .map((y) =&amp;gt; lines.get(y)!.join(" ").trim())&lt;br&gt;
    .filter((line) =&amp;gt; line.length &amp;gt; 0);&lt;br&gt;
}&lt;br&gt;
&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;This same helper turned out to be useful well beyond comparing: PDF to Word, PDF to HTML, PDF to Excel and PDF to EPUB all start from "give me the lines of each page".&lt;/p&gt;

&lt;h2&gt;
  
  
  Diffing
&lt;/h2&gt;

&lt;p&gt;With lines per page, the comparison itself is short. Flatten both documents into one string each and let jsdiff do a line diff:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;ts&lt;br&gt;
import { diffLines, type Change } from "diff";&lt;/p&gt;

&lt;p&gt;export async function comparePdfs(fileA: File, fileB: File): Promise {&lt;br&gt;
  const [pagesA, pagesB] = await Promise.all([getAllPageLines(fileA), getAllPageLines(fileB)]);&lt;br&gt;
  const textA = pagesA.flat().join("\n");&lt;br&gt;
  const textB = pagesB.flat().join("\n");&lt;br&gt;
  return diffLines(textA, textB);&lt;br&gt;
}&lt;br&gt;
&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;&lt;code&gt;diffLines&lt;/code&gt; returns an array of &lt;code&gt;Change&lt;/code&gt; objects with &lt;code&gt;added&lt;/code&gt; / &lt;code&gt;removed&lt;/code&gt; flags, which map directly onto green and red blocks in the UI. Both PDFs are parsed in parallel, and nothing leaves the tab.&lt;/p&gt;

&lt;p&gt;pdf.js itself has to be loaded lazily in Next.js (it touches &lt;code&gt;DOMMatrix&lt;/code&gt; at import time, which breaks server prerendering), so &lt;code&gt;getPdfjs()&lt;/code&gt; does a dynamic &lt;code&gt;import("pdfjs-dist")&lt;/code&gt; the first time it's needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the naive version gets it wrong
&lt;/h2&gt;

&lt;p&gt;This works well for the main use case, contracts and letters that went through a small edit. But "group by y" has sharp edges, and I'd rather list them than pretend they don't exist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Items on a line aren't sorted by x.&lt;/strong&gt; I join them in the order the PDF drew them. Most generators draw left to right, but not all. Sorting each bucket by &lt;code&gt;transform[4]&lt;/code&gt; is the obvious fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact y matching is fragile.&lt;/strong&gt; Superscripts, mixed fonts, or a baseline off by 0.6pt can split one visual line into two. Bucketing with a tolerance (say, a fraction of the font height) instead of &lt;code&gt;Math.round&lt;/code&gt; would be more robust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-column layouts merge.&lt;/strong&gt; Left and right columns at the same y become one "line". You'd need to detect columns by x gaps first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-wrapping looks like a total rewrite.&lt;/strong&gt; If one word is added early in a paragraph and the text re-flows, every following line in the paragraph changes. A line diff reports the whole paragraph as changed. Running a word diff (&lt;code&gt;diffWords&lt;/code&gt;) inside changed blocks would show the actual edit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headers, footers and page numbers add noise&lt;/strong&gt;, especially if pagination shifts by a page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scanned PDFs have no text at all.&lt;/strong&gt; They're images, so there's nothing to diff without OCR first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a text diff, not a visual diff.&lt;/strong&gt; A moved logo or a changed font won't show up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For "did they change the notice period?", the current version does the job. For a 200-page reflowed manual, you'd want 1, 2 and 4 at least.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A PDF stores positioned glyphs, not lines or paragraphs. Any text feature starts by reconstructing structure from coordinates.&lt;/li&gt;
&lt;li&gt;PDF y coordinates grow upwards, so reading order is y descending.&lt;/li&gt;
&lt;li&gt;The diff algorithm is the easy part; the quality of the comparison depends almost entirely on how you rebuild the text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to try it, the tool is at &lt;a href="https://www.handybin.net/compare-pdfs" rel="noopener noreferrer"&gt;Handybin Compare PDFs&lt;/a&gt;. It runs in your browser, so you can open DevTools' Network tab and confirm the files aren't uploaded.&lt;/p&gt;

&lt;p&gt;I'd love to hear how others have handled line reconstruction, especially multi-column layouts.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>pdf</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Trust, but Verify: How We Audited OpenAI's Lean 4 Math Proofs with a Zero-Dependency Sidecar</title>
      <dc:creator>liao-zhizhi</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:24:22 +0000</pubDate>
      <link>https://dev.to/liaozhizhi/trust-but-verify-how-we-audited-openais-lean-4-math-proofs-with-a-zero-dependency-sidecar-283d</link>
      <guid>https://dev.to/liaozhizhi/trust-but-verify-how-we-audited-openais-lean-4-math-proofs-with-a-zero-dependency-sidecar-283d</guid>
      <description>&lt;p&gt;Recently, OpenAI released a repository of formal proofs (&lt;code&gt;ten-proofs&lt;/code&gt;) demonstrating AI's capability to solve complex mathematical problems using Lean 4. &lt;/p&gt;

&lt;p&gt;When an AI lab drops thousands of lines of formal code claiming "we solved it," how does the rest of the world verify it? Currently, we rely on a handful of expert mathematicians who don't have the time to read 37,000 lines of code. Or, we rely on another LLM to "judge" it—which just hallucinates a thumbs-up.&lt;/p&gt;

&lt;p&gt;I wanted to see if an automated, physical-witness approach could do better. So, I used my open-source project, &lt;code&gt;harness-audit-sidecar&lt;/code&gt;, to audit OpenAI's proofs locally. &lt;/p&gt;

&lt;p&gt;Here is what happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Setup: No Hallucinations Allowed
&lt;/h3&gt;

&lt;p&gt;LLM-as-a-Judge fails here. A compiler asks &lt;em&gt;"does this elaborate?"&lt;/em&gt;, but an auditor must ask &lt;em&gt;"is there a physical witness?"&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;To create a physical witness, I didn't just scan the text. I pulled the 7.2 GB Mathlib cache, compiled 8,687 jobs locally, and ran our &lt;code&gt;LeanProofAuditor&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Results: 11 PASS, 12 FAIL
&lt;/h3&gt;

&lt;p&gt;The audit took about 40 minutes. Here are the hard facts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The 11 Main Proof Files achieved a genuine &lt;code&gt;PASS&lt;/code&gt;.&lt;/strong&gt; &lt;br&gt;
All theorems elaborated successfully. Zero smuggled custom axioms. They rely only on the three standard Lean axioms (&lt;code&gt;Classical.choice&lt;/code&gt;, &lt;code&gt;propext&lt;/code&gt;, &lt;code&gt;Quot.sound&lt;/code&gt;). OpenAI's core mathematical proofs are solid.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;12 Files returned &lt;code&gt;FAIL&lt;/code&gt; (42 placeholders found).&lt;/strong&gt;&lt;br&gt;
Wait, did OpenAI cheat? No. These 12 files are located in &lt;code&gt;ComparatorChallenges/&lt;/code&gt;. They are templates intentionally left with &lt;code&gt;sorry&lt;/code&gt; (Lean's unproved placeholder) for human contestants to fill out. Our auditor simply reported the physical fact: &lt;em&gt;these files are not complete proofs.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;p&gt;If an LLM judged this repository, it would read the comments, see the word "solved", and output a confident &lt;code&gt;PASS&lt;/code&gt; for the whole folder—completely ignoring the 42 &lt;code&gt;sorry&lt;/code&gt; placeholders.&lt;/p&gt;

&lt;p&gt;Our sidecar enforces a four-valued verdict: &lt;strong&gt;PASS, FAIL, INVALID, NOT RUN&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It didn't just guess. &lt;/li&gt;
&lt;li&gt;It didn't accept &lt;code&gt;exit code 0&lt;/code&gt; as proof (because a file with &lt;code&gt;sorry&lt;/code&gt; can compile cleanly).&lt;/li&gt;
&lt;li&gt;It inspected the artifact's dependency graph via &lt;code&gt;#print axioms&lt;/code&gt; to expose the truth.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Four-Valued Immune System
&lt;/h3&gt;

&lt;p&gt;This audit is just one application of the &lt;code&gt;harness-audit-sidecar&lt;/code&gt;. Originally, it was built to catch AI agents lying about writing files or executing shell commands. &lt;/p&gt;

&lt;p&gt;We introduced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conditional Immunity&lt;/strong&gt;: Tier 1 (cheap stat checks) and Tier 2 (full Git diffs). Two consecutive &lt;code&gt;FAIL&lt;/code&gt;s trigger an "inflammation storm" that blocks writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Membrane Gateway&lt;/strong&gt;: Agents cannot define their own success criteria. They must register expectations before execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent Metabolism OS&lt;/strong&gt;: Regulates LLM API costs and fast/slow thinking beats.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A Tool that Audits Itself
&lt;/h3&gt;

&lt;p&gt;During development, our own auditor caught &lt;em&gt;itself&lt;/em&gt; producing false completions. We log these in &lt;code&gt;ERRATA.md&lt;/code&gt;. For example, we initially failed a legitimate &lt;code&gt;opaque&lt;/code&gt; definition because we confused it with an &lt;code&gt;axiom&lt;/code&gt;. We corrected it and wrote a regression test. &lt;/p&gt;

&lt;p&gt;An audit tool that only audits others is a decoration. You can read our ERRATA to see how the system caught its own bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/liao-zhizhi/harness-audit-sidecar" rel="noopener noreferrer"&gt;Agent Harness Sidecar (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/liao-zhizhi/agent-metabolism-os" rel="noopener noreferrer"&gt;Agent Metabolism OS (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doi.org/10.5281/zenodo.23891527" rel="noopener noreferrer"&gt;Preprint on Zenodo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>math</category>
      <category>lean4</category>
    </item>
    <item>
      <title>Healthcare AI and the Problem of Downstream Workload</title>
      <dc:creator>Onyedikachi Onwurah</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:24:00 +0000</pubDate>
      <link>https://dev.to/onyedikachi_onwurah_00ba3/healthcare-ai-and-the-problem-of-downstream-workload-2ke7</link>
      <guid>https://dev.to/onyedikachi_onwurah_00ba3/healthcare-ai-and-the-problem-of-downstream-workload-2ke7</guid>
      <description>&lt;p&gt;Healthcare AI is often evaluated by measuring the work it automates. However, an important question is frequently overlooked:&lt;/p&gt;

&lt;p&gt;Does the system reduce workload, or does it transfer that workload to another team?&lt;/p&gt;

&lt;p&gt;Consider an AI triage system that identifies patients requiring additional investigations. The triage department may process patients faster, but the system could generate more laboratory requests, specialist referrals, and administrative tasks.&lt;/p&gt;

&lt;p&gt;The local improvement may conceal a wider operational problem.&lt;/p&gt;

&lt;p&gt;Understanding workload displacement&lt;/p&gt;

&lt;p&gt;Workload displacement occurs when automation reduces tasks in one part of a workflow but creates additional responsibilities elsewhere.&lt;/p&gt;

&lt;p&gt;This can happen through automated referrals, alert generation, clinical review requests, medication reconciliation, documentation, and appointment coordination.&lt;/p&gt;

&lt;p&gt;The individual actions may be appropriate, but their combined operational impact can exceed the capacity of receiving teams.&lt;/p&gt;

&lt;p&gt;Why agentic AI makes this more important&lt;/p&gt;

&lt;p&gt;Agentic AI can initiate multiple actions across connected services. Without appropriate workload awareness, an agent may generate demand faster than healthcare teams can process it.&lt;/p&gt;

&lt;p&gt;For example, an agent could identify patients requiring further investigation, request tests, initiate referrals, and generate follow-up tasks.&lt;/p&gt;

&lt;p&gt;A safe implementation needs to consider who receives each task, whether the receiving team is authorised and equipped to handle it, and what happens when capacity is unavailable.&lt;/p&gt;

&lt;p&gt;What should healthcare organisations measure?&lt;/p&gt;

&lt;p&gt;Evaluation should include task volumes across departments, staff time, referral completion, waiting times, unresolved requests, workload concentration, and delays created by additional demand.&lt;/p&gt;

&lt;p&gt;Baseline measurements should be compared with post-deployment outcomes. Teams receiving AI-generated work should participate in the evaluation rather than being treated as invisible parts of the workflow.&lt;/p&gt;

&lt;p&gt;A better design principle&lt;/p&gt;

&lt;p&gt;Healthcare AI should optimise connected workflows rather than isolated tasks.&lt;/p&gt;

&lt;p&gt;This requires workflow mapping, downstream impact assessment, capacity-aware automation, appropriate prioritisation, and human escalation when demand exceeds available resources.&lt;/p&gt;

&lt;p&gt;The objective is not simply to automate more work. It is to make healthcare delivery more sustainable.&lt;/p&gt;

&lt;p&gt;If you are building or implementing healthcare AI, include downstream workload in your system requirements and evaluation metrics.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>management</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The error is one line. Investigate the five lines around it.</title>
      <dc:creator>Dany Cohen</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:23:31 +0000</pubDate>
      <link>https://dev.to/torxx666/the-error-is-one-line-investigate-the-five-lines-around-it-10ka</link>
      <guid>https://dev.to/torxx666/the-error-is-one-line-investigate-the-five-lines-around-it-10ka</guid>
      <description>&lt;p&gt;An error line tells you that something failed. The useful clue may be three lines earlier: a retry, a connection message, or a request that never reached the next stage.&lt;/p&gt;

&lt;p&gt;This walkthrough uses Flik to keep surrounding events visible while narrowing a log file. It works locally on macOS and Linux. The sample is synthetic, so you can reproduce the interaction without a running service.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Open a small example
&lt;/h2&gt;

&lt;p&gt;Download Flik from the project page and create this small synthetic sample:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; flik-demo.log &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;LOG&lt;/span&gt;&lt;span class="sh"&gt;'
2026-01-15T09:00:00Z INFO service=demo-api request_id=req-demo-9 request accepted
2026-01-15T09:01:00Z WARN service=demo-api request_id=req-demo-9 connection pool exhausted
2026-01-15T09:02:00Z ERROR service=demo-api request_id=req-demo-9 request timeout
2026-01-15T09:03:00Z INFO service=demo-api request_id=req-demo-9 retry scheduled
2026-01-15T10:00:00Z INFO service=demo-api request_id=req-demo-10 request accepted
2026-01-15T10:01:00Z WARN service=demo-api request_id=req-demo-10 connection pool exhausted
2026-01-15T10:02:00Z ERROR service=demo-api request_id=req-demo-10 request timeout
2026-01-15T10:03:00Z INFO service=demo-api request_id=req-demo-10 retry scheduled
&lt;/span&gt;&lt;span class="no"&gt;LOG
&lt;/span&gt;flik flik-demo.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Files open at the end with live following enabled. Press &lt;code&gt;g&lt;/code&gt; if you want to start at the beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Search for the error, including its neighbours
&lt;/h2&gt;

&lt;p&gt;In the main view, press &lt;code&gt;/&lt;/code&gt; to open search. Enter the following smart-find expression, then press Enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//3,2:error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This retains three original lines before each match and two after it. Context lines are shown alongside the match, in the original order. Overlapping context windows are merged instead of duplicating the same lines.&lt;/p&gt;

&lt;p&gt;Read those neighbouring events before adding another condition. Context can change the question from “which line failed?” to “what happened immediately before this request failed?”&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Narrow one condition at a time
&lt;/h2&gt;

&lt;p&gt;Press &lt;code&gt;x&lt;/code&gt; to undo the latest filter. Press &lt;code&gt;3&lt;/code&gt; to add an include filter, or &lt;code&gt;8&lt;/code&gt; for an exclude filter. For example, include &lt;code&gt;timeout&lt;/code&gt;, then exclude &lt;code&gt;cache&lt;/code&gt;. Each rule is a separate step in the stack.&lt;/p&gt;

&lt;p&gt;If the result becomes too narrow, &lt;code&gt;x&lt;/code&gt; removes only the most recent rule. &lt;code&gt;Ctrl+F&lt;/code&gt; opens the filter stack so you can inspect what is active. That matters when an empty view means “my filters excluded it”, rather than “the event never happened”.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Recover a wider window
&lt;/h2&gt;

&lt;p&gt;Select a result and press &lt;code&gt;Ctrl+G&lt;/code&gt; to inspect its original context. Use &lt;code&gt;n&lt;/code&gt; and &lt;code&gt;p&lt;/code&gt; inside that view to move between matches; Escape returns to the file. The command palette also accepts &lt;code&gt;context 20&lt;/code&gt; when you want 20 original lines before and after a selected event.&lt;/p&gt;

&lt;p&gt;For a live stream, the starting point can be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; demo-api | flik -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service name in that example needs to match your own container. Begin with the downloadable file if you do not have one running.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does—and what it cannot establish
&lt;/h2&gt;

&lt;p&gt;The useful result is an inspectable sequence of events. Adjacent lines alone do not prove a cause: requests can interleave, clocks can differ, and output can be buffered. Check request identifiers and timestamps before drawing a conclusion.&lt;/p&gt;

&lt;p&gt;Flik covers interactive filtering, context and following; keep jq for arbitrary JSON transformations and scripted pipelines. The goal is to make a manual investigation easier to inspect and revise.&lt;/p&gt;

&lt;p&gt;Watch the short demonstration, then repeat the search on the sample:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://flik.spedo.dev?utm_source=dev-to&amp;amp;utm_medium=social&amp;amp;utm_campaign=flik-launch&amp;amp;utm_content=post-21" rel="noopener noreferrer"&gt;https://flik.spedo.dev?utm_source=dev-to&amp;amp;utm_medium=social&amp;amp;utm_campaign=flik-launch&amp;amp;utm_content=post-21&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>debugging</category>
      <category>tools</category>
    </item>
    <item>
      <title>json.dumps Writes NaN by Default: 31 LLM Reports JSON.parse Rejected</title>
      <dc:creator>jidonglab</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:22:33 +0000</pubDate>
      <link>https://dev.to/ji_ai/jsondumps-writes-nan-by-default-31-llm-reports-jsonparse-rejected-2hnk</link>
      <guid>https://dev.to/ji_ai/jsondumps-writes-nan-by-default-31-llm-reports-jsonparse-rejected-2hnk</guid>
      <description>&lt;p&gt;Nine people emailed me the same screenshot: a spinner and the line "Your report is being prepared." One of them had been looking at it for 51 hours.&lt;/p&gt;

&lt;p&gt;The report was done. It had been done since about 40 seconds after their session ended. It was sitting in object storage as a perfectly normal-looking file. The problem was three characters inside it: &lt;code&gt;NaN&lt;/code&gt;. &lt;strong&gt;Python json.dumps writes NaN by default&lt;/strong&gt;, the browser's &lt;code&gt;JSON.parse&lt;/code&gt; refuses to read it, and my pipeline had no step anywhere that cared about the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python's &lt;code&gt;json.dumps&lt;/code&gt; has &lt;code&gt;allow_nan=True&lt;/code&gt; by default, so &lt;code&gt;float('nan')&lt;/code&gt; becomes a bare &lt;code&gt;NaN&lt;/code&gt; token. That is not valid JSON.&lt;/li&gt;
&lt;li&gt;Python's own &lt;code&gt;json.loads&lt;/code&gt; reads &lt;code&gt;NaN&lt;/code&gt; back without complaint, so round-trip tests pass. JavaScript's &lt;code&gt;JSON.parse&lt;/code&gt; throws a &lt;code&gt;SyntaxError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;My NaN came from &lt;code&gt;np.nanmean&lt;/code&gt; over a rubric category where the LLM scorer returned &lt;code&gt;null&lt;/code&gt; for every answer. An all-NaN slice returns NaN and only emits a &lt;code&gt;RuntimeWarning&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;31 of 1,940 reports (1.6%) over 16 days were unreadable. 26 of the 31 came from one interviewer style.&lt;/li&gt;
&lt;li&gt;Fix: score fields as &lt;code&gt;Optional[float]&lt;/code&gt; with &lt;code&gt;allow_inf_nan=False&lt;/code&gt; in Pydantic, plus &lt;code&gt;json.dumps(..., allow_nan=False)&lt;/code&gt; as a tripwire at every write.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What broke?
&lt;/h2&gt;

&lt;p&gt;The system is Preterview, an interview practice platform I run (full disclosure: I built it, &lt;a href="https://preterview.com/en" rel="noopener noreferrer"&gt;preterview.com/en&lt;/a&gt;). A candidate does a voice interview with one of three interviewer styles, an LLM scores each answer against a rubric, and a worker assembles everything into a written report.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftd5zed80xfjpv7qjalsn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftd5zed80xfjpv7qjalsn.jpg" alt="Preterview — an interview session in progress" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The report pipeline looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The LLM scores each answer per rubric category (communication, technical depth, structure, and so on) and returns structured JSON.&lt;/li&gt;
&lt;li&gt;A Python worker aggregates scores per category.&lt;/li&gt;
&lt;li&gt;The worker writes &lt;code&gt;report.json&lt;/code&gt; to object storage.&lt;/li&gt;
&lt;li&gt;The frontend fetches that file directly and renders it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 4 is where 31 reports died. The fetch succeeded, &lt;code&gt;JSON.parse&lt;/code&gt; threw, my error handler caught it, and the error handler's fallback was "show the preparing state and poll again." Forever. I had built a very polite infinite loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where did the NaN come from?
&lt;/h2&gt;

&lt;p&gt;The NaN came from averaging a category that had no scores at all. The scorer prompt allows the model to say "I can't assess this category from this answer" by returning &lt;code&gt;null&lt;/code&gt;. That's deliberate. I'd rather get a &lt;code&gt;null&lt;/code&gt; than a confident 6/10 for system design on an answer about a team conflict.&lt;/p&gt;

&lt;p&gt;The aggregation code turned those nulls into NaN on purpose so they'd be skipped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;category_mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nanmean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;np.nanmean&lt;/code&gt; ignores NaN values. That's what I wanted. What I hadn't thought through is what it does when &lt;em&gt;every&lt;/em&gt; value is NaN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nanmean&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nb"&gt;RuntimeWarning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mean&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;empty&lt;/span&gt; &lt;span class="nb"&gt;slice&lt;/span&gt;
&lt;span class="n"&gt;nan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No exception. A warning and a NaN.&lt;/p&gt;

&lt;p&gt;When does every answer in a category get &lt;code&gt;null&lt;/code&gt;? When the interviewer never asks a question that touches that category. One of my three interviewer styles is a conversational, culture-fit style. It often runs a whole session without a single technical question. So the "technical depth" category came back all-null, &lt;code&gt;nanmean&lt;/code&gt; returned NaN, and the NaN flowed downstream. That's why 26 of the 31 broken reports came from that one style.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does Python json.dumps write NaN by default?
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;allow_nan&lt;/code&gt; defaults to &lt;code&gt;True&lt;/code&gt;. The Python docs are upfront about it: with &lt;code&gt;allow_nan=True&lt;/code&gt;, out-of-range floats are written as their JavaScript equivalents (&lt;code&gt;NaN&lt;/code&gt;, &lt;code&gt;Infinity&lt;/code&gt;, &lt;code&gt;-Infinity&lt;/code&gt;), and the docs note this is not strictly JSON compliant. RFC 8259 has no representation for NaN at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;technical_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;technical_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: NaN}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part that let this survive testing: Python reads it back just fine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;technical_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: NaN}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;technical_depth&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My test suite had a round-trip test: build a report, dump it, load it, compare. Python on both ends, so it passed. The only consumer that enforced the actual JSON spec was the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"technical_depth": NaN}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// SyntaxError: Unexpected token 'N'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why didn't my logs show anything?
&lt;/h2&gt;

&lt;p&gt;Two reasons, and both are defaults.&lt;/p&gt;

&lt;p&gt;First, Python's default warnings filter shows a given warning once per code location, not once per occurrence. My workers are long-running processes. The first all-null category after a deploy printed &lt;code&gt;Mean of empty slice&lt;/code&gt; to stderr. The next 30 printed nothing. Across 16 days I found 11 instances of that warning in the logs, one per worker start, sitting between thousands of normal lines. Nothing about it said "a user's report is broken."&lt;/p&gt;

&lt;p&gt;Second, the worker considered the job successful. &lt;code&gt;json.dumps&lt;/code&gt; returned a string, the upload returned 200, the job was marked done. From the backend's point of view, every one of the 1,940 reports succeeded.&lt;/p&gt;

&lt;p&gt;There was one more trap I tripped over while debugging. My first attempt to display the score did this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cannot&lt;/span&gt; &lt;span class="n"&gt;convert&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="n"&gt;NaN&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;integer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;round(float("nan"), 1)&lt;/code&gt; quietly returns &lt;code&gt;nan&lt;/code&gt;. The report used one decimal place, so this path never raised either. Every layer had a way to pass NaN along without anyone noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which serializers accept NaN and which reject it?
&lt;/h2&gt;

&lt;p&gt;This is the table I wish I'd had. Same input, &lt;code&gt;float("nan")&lt;/code&gt;, very different behavior:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;json.dumps&lt;/code&gt; (stdlib)&lt;/td&gt;
&lt;td&gt;Writes &lt;code&gt;NaN&lt;/code&gt;. Invalid JSON, no error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;json.dumps(..., allow_nan=False)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Raises &lt;code&gt;ValueError&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;json.loads&lt;/code&gt; (stdlib)&lt;/td&gt;
&lt;td&gt;Accepts &lt;code&gt;NaN&lt;/code&gt;, returns &lt;code&gt;nan&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starlette / FastAPI &lt;code&gt;JSONResponse&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Raises &lt;code&gt;ValueError&lt;/code&gt; (it uses &lt;code&gt;allow_nan=False&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pydantic v2 &lt;code&gt;model_dump_json()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Writes &lt;code&gt;null&lt;/code&gt; by default (&lt;code&gt;ser_json_inf_nan&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;orjson&lt;/td&gt;
&lt;td&gt;Writes &lt;code&gt;null&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres &lt;code&gt;jsonb&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Rejects the input as invalid JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser &lt;code&gt;JSON.parse&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Throws &lt;code&gt;SyntaxError&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The irony: if this report had gone through my FastAPI endpoint, it would have been a loud 500 on day one. If I'd stored it in a &lt;code&gt;jsonb&lt;/code&gt; column, the insert would have failed. I chose to write it straight to object storage with the stdlib serializer, which happens to be the most permissive path in the whole table.&lt;/p&gt;

&lt;p&gt;Also notice that "writes &lt;code&gt;null&lt;/code&gt;" is not automatically safe. Pydantic and orjson would have produced valid JSON, but the frontend would then have rendered "Technical depth: null" or crashed on &lt;code&gt;null.toFixed(1)&lt;/code&gt;. Valid syntax, wrong meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you stop NaN from reaching your JSON?
&lt;/h2&gt;

&lt;p&gt;Make "not assessed" a real value instead of a float that pretends to be a number. Then make every serializer refuse NaN so the next mistake fails at write time, not in a user's browser.&lt;/p&gt;

&lt;p&gt;The model change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CategoryScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_inf_nan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assessed_answers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the aggregation stops producing NaN in the first place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;category_mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No numpy, no warning, and the empty case is a branch you can read instead of a side effect you have to remember.&lt;/p&gt;

&lt;p&gt;Then the tripwire, at the single function that writes report files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;allow_nan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If anything upstream ever sneaks a NaN or an Infinity back in, this raises, the job fails, and my alerting sees a failed job instead of a successful one.&lt;/p&gt;

&lt;p&gt;Two smaller changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests run with warnings as errors.&lt;/strong&gt; &lt;code&gt;filterwarnings = error::RuntimeWarning&lt;/code&gt; in pytest config. The "Mean of empty slice" warning would have failed CI on the first test with a skipped category, and I had one. It just never asserted anything about the output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The frontend stops polling on parse errors.&lt;/strong&gt; A &lt;code&gt;SyntaxError&lt;/code&gt; is not "still preparing." It now shows an error state and pings me.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend change was really the bigger lesson. The NaN was a bug. The spinner turned that bug into 51 hours of a user waiting for something that was never going to arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did I recover the 31 broken reports?
&lt;/h2&gt;

&lt;p&gt;I re-ran aggregation only. The raw per-answer LLM scores were stored separately from the assembled report, so I didn't need a single new model call. All 31 reports rebuilt and rendered in under two minutes, with "Technical depth: not assessed in this session" where the NaN used to be. I emailed the nine people who'd written in, plus the other 22 who'd quietly given up.&lt;/p&gt;

&lt;p&gt;If I'd only stored the final report, I'd have had to re-score 31 sessions and explain why their scores changed. Keep your raw LLM outputs. You will need to re-derive something from them eventually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Python json.dumps write NaN by default?
&lt;/h2&gt;

&lt;p&gt;Yes. Python's &lt;code&gt;json.dumps&lt;/code&gt; uses &lt;code&gt;allow_nan=True&lt;/code&gt; by default, so &lt;code&gt;float('nan')&lt;/code&gt; is written as a bare &lt;code&gt;NaN&lt;/code&gt; token, which is not valid JSON under RFC 8259. Python's &lt;code&gt;json.loads&lt;/code&gt; accepts it back, so Python-only tests won't catch it, but JavaScript's &lt;code&gt;JSON.parse&lt;/code&gt;, Postgres &lt;code&gt;jsonb&lt;/code&gt;, and most strict parsers will reject the file. Pass &lt;code&gt;allow_nan=False&lt;/code&gt; to make serialization raise a &lt;code&gt;ValueError&lt;/code&gt; instead, and represent missing numeric values as &lt;code&gt;None&lt;/code&gt;/&lt;code&gt;null&lt;/code&gt; explicitly, especially for LLM-generated scores where "couldn't assess" is a legitimate answer that averaging code will happily turn into NaN.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by the developer behind &lt;a href="https://preterview.com/en" rel="noopener noreferrer"&gt;Preterview&lt;/a&gt;, an interview prep platform.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>CVE-2026-108452 exposure and asset inventory: what 163,243 SharePoint fingerprints mean</title>
      <dc:creator>StarkMan</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:21:04 +0000</pubDate>
      <link>https://dev.to/stark_zhuang_df5076f35c68/cve-2026-108452-exposure-and-asset-inventory-what-163243-sharepoint-fingerprints-mean-5495</link>
      <guid>https://dev.to/stark_zhuang_df5076f35c68/cve-2026-108452-exposure-and-asset-inventory-what-163243-sharepoint-fingerprints-mean-5495</guid>
      <description>&lt;h1&gt;
  
  
  CVE-2026-108452 exposure and asset inventory: what 163,243 SharePoint fingerprints mean
&lt;/h1&gt;

&lt;p&gt;External search can make a vulnerability feel larger than the evidence supports. ZoomEye returned 163,243 matches for &lt;code&gt;app="Microsoft SharePoint"&lt;/code&gt; when the query was checked on November 3, 2026. That number is useful for attack-surface discovery, but it is not a count of 163,243 vulnerable servers. The CVE-specific filter returned 0, and the product query does not reveal which build or configuration each asset runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The vulnerability
&lt;/h2&gt;

&lt;p&gt;CVE-2026-108452 is a Microsoft SharePoint path traversal issue classified as CWE-22. Microsoft states that an authorized attacker can use it to execute code over the network. The CVSS 3.1 score is 8.8, with low privileges required, low attack complexity, and no user interaction. Confidentiality and integrity impacts are High.&lt;/p&gt;

&lt;p&gt;The affected product is Microsoft SharePoint Server Subscription Edition on x64 systems. The vulnerable range starts at 16.0.0 and ends before 16.0.20326.20190. KB5002927, released October 13, 2026, updates the product to build 16.0.20326.20190.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the search results
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;app="Microsoft SharePoint"&lt;/code&gt; is a product-fingerprint query. A match means ZoomEye observed evidence that looks like SharePoint under the chosen search scope. It does not verify the edition, installed cumulative update, application pool identity, workflow configuration, or whether a low-privileged account can reach the vulnerable path.&lt;/p&gt;

&lt;p&gt;The separate filter &lt;code&gt;vul.cve="CVE-2026-108452"&lt;/code&gt; returned 0. A zero CVE-index result means the CVE was not associated with any matching asset in that query at that time. It does not prove that no vulnerable systems exist. The CVE may be too new, the asset record may not identify the build, or the search engine may not index the relevant version detail.&lt;/p&gt;

&lt;p&gt;The correct interpretation is therefore a two-layer statement. First, the broad product population is measurable at 163,243 fingerprint matches. Second, the confirmed vulnerable population is unknown from this evidence alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning Exposure into an inventory task
&lt;/h2&gt;

&lt;p&gt;Teams should export the public-facing candidate set into an internal review queue. For each host that belongs to the organization, verify the product edition, server build, farm role, and patch history. A SharePoint front end, application server, and workflow component may have different install states.&lt;/p&gt;

&lt;p&gt;The fixed build is concrete: 16.0.20326.20190. Microsoft's support guidance also calls for KB5002799 before KB5002927 when SharePoint Workflow Manager is present. A classic Workflow Manager installation requires the stated debug flag step. These dependencies belong in the patch plan because a partially updated farm can create false confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prioritization
&lt;/h2&gt;

&lt;p&gt;Prioritize systems that are both externally reachable and below the fixed build. Increase priority when the farm accepts partner, contractor, guest, or delegated application access. The vendor says an authorized attacker is required, so the privilege boundary is part of the exposure model. A server reachable only by tightly managed administrators has a different risk profile from a collaboration farm with broad low-privileged access.&lt;/p&gt;

&lt;p&gt;Use asset ownership and business function as decision inputs. A customer-facing SharePoint portal, an extranet, and an isolated internal archive may all match the product fingerprint, but they do not carry the same consequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to do
&lt;/h2&gt;

&lt;p&gt;Do not publish the broad product count as a victim count. Do not infer patch state from a title, favicon, or HTTP header. Do not close an asset review because the CVE filter returned 0. The filter is a useful index signal, not a substitute for build inspection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;Apply KB5002927 according to Microsoft's deployment instructions and verify that every farm node reports the fixed build. Until completion, limit untrusted network access to SharePoint management and authoring surfaces and review low-privileged or delegated identities that can reach the farm. These steps reduce exposure, but the vendor update is the substantive remediation identified in the sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Microsoft Security Update Guide, &lt;a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-108452" rel="noopener noreferrer"&gt;CVE-2026-108452&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft Support, &lt;a href="https://support.microsoft.com/en-us/servicing/office/hotfix/sharepoint/5002927" rel="noopener noreferrer"&gt;KB5002927&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft Learn, &lt;a href="https://learn.microsoft.com/en-us/officeupdates/sharepoint-updates" rel="noopener noreferrer"&gt;SharePoint updates&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CVE Program, &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-108452" rel="noopener noreferrer"&gt;CVE-2026-108452&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>cybersecurity</category>
      <category>infosec</category>
      <category>microsoft</category>
      <category>security</category>
    </item>
    <item>
      <title>Nine Keys Can SSH Into Production. Five Owners Don't Work Here Anymore.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:20:41 +0000</pubDate>
      <link>https://dev.to/dhruonweb/nine-keys-can-ssh-into-production-five-owners-dont-work-here-anymore-88g</link>
      <guid>https://dev.to/dhruonweb/nine-keys-can-ssh-into-production-five-owners-dont-work-here-anymore-88g</guid>
      <description>&lt;p&gt;I cat'd authorized_keys on our oldest box for a boring reason and kept reading: the 2023 contractor, a dead CI pipeline, two ex-engineers, an 'ops-temp' nobody can attribute. The sweep, the smoke alarm, the one-week evidence trick, and keys-that-expire culture.&lt;/p&gt;

&lt;p&gt;I opened the file for a boring reason — a connectivity weirdness — and then just kept reading. ~/.ssh/authorized_keys on our oldest production box: nine lines, nine doors.&lt;/p&gt;

&lt;p&gt;Matching comments to memory: the 2023 migration contractor (still there), the CI pipeline decommissioned last spring (still there), two engineers who left — one warmly, one not (still there), a key commented ops-temp that nobody including our ops lead can attribute to a human, and one line with no comment at all, which is its own small horror genre.&lt;/p&gt;

&lt;p&gt;Four keys belonged to current humans who still needed access. Five were sediment. The door remembered everyone, because nobody had ever asked it to forget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;authorized_keys is an attendance sheet&lt;/strong&gt;&lt;br&gt;
The oldest access-control file in our profession is a plain-text list on disk: appended to for a decade, reviewed never. Every line was added on someone's worst day — incident, deadline, "just for tonight, add my key." Keys arrive during emergencies; emergencies don't do cleanup; departures miss the file entirely, because it isn't in any system. It's wallpaper. And unlike a password, a key never expires out of embarrassment — it waits for its laptop to be sold, lost, or inherited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sweep&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# keys-audit.sh — every authorized_keys on every box, with its "who/why" comments&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;host &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;hosts.txt&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;ssh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'for f in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
    [ -f "$f" ] &amp;amp;&amp;amp; {
      echo "== $HOSTNAME $f — $(grep -cvE "^[[:space:]]*$|^[[:space:]]*#" "$f") keys"
      grep -vE "^[[:space:]]*$|^[[:space:]]*#" "$f" | awk "{print \"   \", \$NF}"
    }; done'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;tee &lt;/span&gt;keys-audit.txt
&lt;span class="c"&gt;# then the human step: diff the comment column against the current roster.&lt;/span&gt;
&lt;span class="c"&gt;# every line that doesn't match a living owner is sediment.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ours diffed to five. Yours will too; that number is your real access list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four changes that stuck&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keys got names and reasons. One comment format, enforced at add-time:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;ana.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ana@laptop — incident-4821 — 2026-11-03"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /home/ubuntu/.ssh/authorized_keys
&lt;span class="c"&gt;# a key that can't state its reason in six words doesn't get added.&lt;/span&gt;
&lt;span class="c"&gt;# 'ops-temp' failed this test immediately and was deleted with a small ceremony.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Offboarding gained a server step. Badge, laptop, SSO — and this, on every box:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'ana@'&lt;/span&gt; /root/.ssh/authorized_keys /home/&lt;span class="k"&gt;*&lt;/span&gt;/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The step exists because the 2023 contractor taught us it doesn't happen by osmosis.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The file got a smoke alarm. Any change pages someone — not blocks, pages; additions are sometimes legitimate at 2 a.m., silent additions never are:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# auditd: watch writes and attributes to authorized_keys on every box&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'-w /root/.ssh/authorized_keys -p wa -k ssh_sediment'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/audit/rules.d/ssh.rules
&lt;span class="nb"&gt;sudo &lt;/span&gt;augenrules &lt;span class="nt"&gt;--load&lt;/span&gt;
&lt;span class="c"&gt;# wire the audit event to your pager; our worst file became our best-monitored one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Deletion ran on evidence, not folklore. We almost kept the unattributed key "in case something depends on it." Instead: a week of fingerprints.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^#LogLevel INFO/LogLevel VERBOSE/'&lt;/span&gt; /etc/ssh/sshd_config &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload ssh
&lt;span class="c"&gt;# after a week — which fingerprints actually authenticated?&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; ssh &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"-7 days"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'SHA256:[A-Za-z0-9+/=]+'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; used.txt
&lt;span class="c"&gt;# and per line in the file — which keys never appeared in used.txt?&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1" "$2}'&lt;/span&gt; /root/.ssh/authorized_keys | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; t k&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;fp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | ssh-keygen &lt;span class="nt"&gt;-lf&lt;/span&gt; - 2&amp;gt;/dev/null | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$fp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; used.txt &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"UNUSED ALL WEEK: &lt;/span&gt;&lt;span class="nv"&gt;$fp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing used it. Measure before you mourn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New boxes start at one&lt;/strong&gt;&lt;br&gt;
The habit stuck because new machines made it easy: on the Cubes we run at Krova Cloud, a box boots with exactly one key — the one handed at creation — and root login is off by default. Additions become a decision you make, not sediment you inherit. Humans increasingly skip static keys entirely: short-lived certs through a bastion for people, per-pipeline keys rotated with the pipeline for robots. Zero sediment, not zero keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static keys aren't a sin. Plenty of honest estates run them fine because they run the reviews. The sin is the unreviewed list, not the mechanism.&lt;/li&gt;
&lt;li&gt;Ceremony has a cost. Certs and bastions add moving parts and one more thing to fail during the incident that justified the shortcut. We tiered: short-lived access on sensitive boxes, named-and-alarmed keys elsewhere. Perfection is a budget line, not a value.&lt;/li&gt;
&lt;li&gt;The alarm will page you once legitimately, at 2 a.m., during a real incident. Thank it. That page is the system proving it can still tell the difference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The door remembers. You don't have to.&lt;/strong&gt;&lt;br&gt;
Go cat the file — the actual one, on the actual oldest box, right now. Count the lines; count the owners who are gone. Ours was nine and five. We deleted four, adopted one (the CI key got a name, a rotation, and an apology), and the ninth — the comment-less line — turned out to be me, from a forgotten 2022 incident.&lt;/p&gt;

&lt;p&gt;The door remembered me too.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>cybersecurity</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Choose an RK3588, RK3576, RK3568, or RK3566 Core Board</title>
      <dc:creator>ZedIoT</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:20:00 +0000</pubDate>
      <link>https://dev.to/zediot/how-to-choose-an-rk3588-rk3576-rk3568-or-rk3566-core-board-16l1</link>
      <guid>https://dev.to/zediot/how-to-choose-an-rk3588-rk3576-rk3568-or-rk3566-core-board-16l1</guid>
      <description>&lt;h1&gt;
  
  
  How to Choose an RK3588, RK3576, RK3568, or RK3566 Core Board
&lt;/h1&gt;

&lt;p&gt;Suppose a device needs two cameras, two wired network connections, and a display. A supplier sends back a one-page RK3588 or RK3576 specification headed "6 TOPS, eight cores." Neither number answers the first design question: &lt;strong&gt;can the particular core board and carrier board run those interfaces together?&lt;/strong&gt; A SoC may expose a controller that the module does not bring to its connector, or the carrier may route a high-speed lane to a different peripheral. For a single-display controller with one Ethernet port and modest local inference, starting with RK3588 by default may buy capability the product cannot use.&lt;/p&gt;

&lt;p&gt;The useful first pass is &lt;strong&gt;elimination, not a four-chip performance ladder&lt;/strong&gt;. List the interfaces and software constraints that cannot be traded away. Use Rockchip's public SoC documentation to narrow the field. Then request evidence for the exact module revision and carrier design, and test sustained operation in the product enclosure. This article supports that documentation-stage shortlist. It is not a measured comparison of frame rate, power, temperature, unit price, or availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with interfaces that must operate at the same time
&lt;/h2&gt;

&lt;p&gt;Write a &lt;strong&gt;simultaneous-use requirement&lt;/strong&gt;, such as "two camera inputs + two physical Ethernet ports + NVMe storage + one touch display," rather than four independent checkboxes. The camera path still has to coexist with display, video processing, storage, and network traffic at the required resolution and format. A controller on the SoC block diagram is not proof that the module exposes it; a signal on the module connector is not proof that the carrier provides a usable port. Extra NPU capacity cannot create a lane already assigned elsewhere.&lt;/p&gt;

&lt;p&gt;The distinction is especially important for &lt;strong&gt;shared high-speed resources&lt;/strong&gt;. Rockchip's RK3576 product page lists USB 3.0, PCIe 2.1, and SATA among combo interface options. Those labels should not be counted as independent ports in the same configuration. The RK3566 brief datasheet likewise describes shared high-speed resources. Ask the board supplier to map the exact target peripherals to pins and mux selections before freezing a design.&lt;/p&gt;

&lt;p&gt;Software can eliminate a candidate even when the wiring works. "Boots Linux" is weaker than "the supplier's current BSP supports this camera sensor, display panel, network PHY, and NPU runtime together." Record the module and carrier revisions, kernel branch, device tree, image version, driver ownership, and patch path. A boot demo proves only the first step. Products with field updates and component substitutions need a maintainable software combination, not an isolated demonstration.&lt;/p&gt;

&lt;p&gt;The decision path separates documentation screening from product testing. Failure at either hardware gate ends the current board proposal; passing them only qualifies a module-and-carrier combination for a reproducible software check and sample test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each SoC belongs on an initial shortlist
&lt;/h2&gt;

&lt;p&gt;The following rows identify plausible evaluation tasks and the first board-level question for each chip. They do not rank four tested products under one workload.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Reason to evaluate it&lt;/th&gt;
&lt;th&gt;First question for the module vendor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RK3588&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multiple high-resolution vision inputs, complex displays, or substantial high-speed expansion&lt;/td&gt;
&lt;td&gt;Which camera, display, PCIe/storage, and Ethernet paths are exposed concurrently, and how is the enclosure cooled?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RK3576&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A display-and-vision terminal with industrial connections&lt;/td&gt;
&lt;td&gt;How are combo lanes assigned, are both network paths and CAN FD exposed on this board revision, and which BSP is supported?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RK3568&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A gateway, HMI, or lighter vision device that still needs networking and expansion&lt;/td&gt;
&lt;td&gt;What do the module connector and carrier actually provide for Ethernet, PCIe/SATA, image support, and temperature range?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RK3566&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A single-screen or lighter edge-control device where limited expansion is acceptable&lt;/td&gt;
&lt;td&gt;Are its exposed peripherals, shared lanes, memory, and runtime headroom sufficient for the target application?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  RK3588 and RK3576 share a headline NPU figure, not a system profile
&lt;/h3&gt;

&lt;p&gt;The RK3588 product page describes &lt;strong&gt;four Cortex-A76 and four A55 cores&lt;/strong&gt;, multiple camera inputs, high-resolution media capabilities, and a &lt;strong&gt;6 TOPS NPU&lt;/strong&gt;. Rockchip describes RK3576 with &lt;strong&gt;four A72 and four A53 cores&lt;/strong&gt;, a 6 TOPS NPU, multiple display options, CAN FD, and two RGMII interfaces. Equal advertised NPU figures do not establish equal operator coverage, CPU preprocessing time, memory behavior, video throughput, or sustained performance.&lt;/p&gt;

&lt;p&gt;For a product dominated by several camera streams, complex display output, and fast storage, include an RK3588 board in the test set. For a display-focused industrial terminal that also needs network and field-bus connections, an RK3576 board deserves evaluation. These are reasons to &lt;strong&gt;request pinout and BSP evidence&lt;/strong&gt;, not promises about which finished device will perform better.&lt;/p&gt;

&lt;p&gt;Do not transfer a carrier's connector list to every board using the same SoC. Firefly, for example, describes Core-3588JD4 together with the MB-JD4-RK3588 carrier in its AIO development board; HDMI, serial, camera, and other accessible connectors belong to that defined combination. Another RK3588 module can have a different board-to-board connector, power design, image, or manufacturing revision.&lt;/p&gt;

&lt;h3&gt;
  
  
  RK3568 and RK3566 need separate network and expansion checks
&lt;/h3&gt;

&lt;p&gt;The RK3568 brief datasheet and RK3566 brief datasheet both describe &lt;strong&gt;quad-core A55&lt;/strong&gt; devices with media and edge-compute resources. That common ground puts them in the same early screening exercise, but does not make their boards interchangeable. If a gateway needs two independent wired networks plus high-speed storage and expansion, ask an RK3568 supplier for the exact port and lane map. If one physical network and modest peripherals meet the requirement, an RK3566 module may remain in scope. Neither conclusion establishes price or power savings; those need like-for-like quotations and measurements.&lt;/p&gt;

&lt;p&gt;Firefly's Core-3568J and Core-3566JD4 documentation shows how a specific module is paired with a carrier to make a usable development system. It is a useful prompt for supplier questions, not a universal specification for all RK3568 or RK3566 boards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three pieces of evidence turn a chip shortlist into a board shortlist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;First, request an interface and concurrency map for the exact core-board revision.&lt;/strong&gt; A pinout, board-to-board connector definition, carrier schematic, or supplier-signed interface matrix should show camera lanes, display outputs, PHYs, PCIe, SATA, USB, CAN, serial, and debug paths in one place. Mark mux alternatives explicitly. A design supported only by a SoC block diagram, without evidence that the board exposes the required combination, is not ready for board freeze.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, request a reproducible software set&lt;/strong&gt;: module and carrier revisions, bootloader, kernel and device tree, Linux or Android image, NPU runtime, relevant camera and display drivers, and the supplier's update channel. Ask for a minimal image and test steps for the functions the product will actually ship. For "two cameras, local inference, and network upload," a camera preview and an unrelated NPU demo are not an end-to-end validation. Acquisition, preprocessing, inference, output, and error recovery need to run in one versioned setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, define the sustained-operation boundary.&lt;/strong&gt; Published operating-temperature ranges and heatsink recommendations do not predict temperature inside the final enclosure. At the intended ambient temperature, record frequency, power, dropped frames, restart behavior, and component temperatures while the actual peripherals run together. A gateway also needs tests for network loss, power loss, storage wear, and recovery. Until those records exist, the honest conclusion is &lt;strong&gt;"eligible for evaluation," not "production-ready."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply the evidence to the product, not to a chip hierarchy
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Multi-camera industrial vision device.&lt;/strong&gt; Give RK3588 and RK3576 board suppliers the same sensor, resolution, encoding, display, network, and storage workload. Compare their pinout and lane allocation first. Then run the same model and runtime version on candidate hardware inside a representative thermal setup. Decide whether RK3568 or RK3566 should leave the list based on the target chain, not a presumed market tier. Both six-TOPS labels leave end-to-end frame rate and cooling unresolved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dual-network gateway with a local HMI.&lt;/strong&gt; Establish whether the networks must be physically independent and whether cellular, CAN, isolated serial, or maintainable storage is also required. RK3568 and RK3576 are reasonable documentation-stage candidates; a single-network, single-screen product may also retain RK3566. Require a board-specific PHY and connector map, then test the actual protocols and recovery behavior. CPU or NPU peaks are poor substitutes for wiring, power, and update evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-display controller.&lt;/strong&gt; Ask whether multiple video streams, fast storage, or complex inference are genuine requirements. If not, begin with specific RK3566 or RK3568 modules and add RK3576 or RK3588 only when an identified display, vision, or industrial interface exceeds their board-level resources. Do not infer lower price or power from the chip name. Quotations become comparable only after memory, storage, carrier, cooling, quantity, and support terms are held constant.&lt;/p&gt;

&lt;h2&gt;
  
  
  When none of the four is the right starting point
&lt;/h2&gt;

&lt;p&gt;A product doing &lt;strong&gt;simple sensing and control&lt;/strong&gt;, without Linux-class applications or media processing, may be better served by an MCU or a simpler platform. If the hard constraint is &lt;strong&gt;cellular certification, electrical isolation, deterministic control, or long-term supply&lt;/strong&gt;, select around that system boundary first. Familiarity with Rockchip is not a reason to force one of these application processors into the design. Conversely, if a specific peripheral driver or NPU operator is mandatory and the available BSP cannot establish support, a generic "Linux supported" statement should not keep a board on the shortlist.&lt;/p&gt;

&lt;p&gt;The useful output of this phase is a &lt;strong&gt;record of rejected candidates and why they failed&lt;/strong&gt;, plus no more than two module-and-carrier combinations for sample testing. Attach each combination's interface map, software versions, power and cooling proposal, and the workload that must be measured. The next discussion can then be about a reproducible product configuration rather than an abstract claim that one chip is "stronger."&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and scope
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Rockchip RK3588 product specifications&lt;/li&gt;
&lt;li&gt;Rockchip RK3576 product specifications&lt;/li&gt;
&lt;li&gt;Rockchip RK3568 brief datasheet&lt;/li&gt;
&lt;li&gt;Rockchip RK3566 brief datasheet&lt;/li&gt;
&lt;li&gt;Firefly Core-3588JD4, Core-3576JD4, Core-3568J, and Core-3566JD4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conclusions above are limited to public SoC and board documentation. They do not cover the target module's complete connector definition, current BSP release notes, orderable part number, or a like-for-like workload measurement. The product scenarios are selection exercises, not results from a deployment.&lt;/p&gt;

</description>
      <category>iot</category>
      <category>architecture</category>
      <category>performance</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
