手書きコーディングに戻ろうとしています。

2026/05/11 10:23

手書きコーディングに戻ろうとしています。

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

k10s(NVIDIA クラスター運用者向けの GPU 意識型 Kubernetes ダッシュボード)の構築から得られた主な教訓は、AI は機能の迅速な提供に優れている一方で、システムアーキテクチャにおいては頻繁に失敗し、倒壊しやすいコードベースを導き込む点にある。Go と Bubble Tea フレームワークを用いた「vibe-coded」アプローチで 30 週間週末にわたり開発を進めたチームは、7 ヶ月間で 234 コミットを実現したにもかかわらず、深刻な構造的欠陥が蓄積しており、最終的にこの作業の約 70% が破棄された。これには

model.go
に収められたコード行を約 1,690 行も含まれている。プロジェクトは以下の 5 つの批判的アーキテクチャ上の失敗に直面した:AI がシステム不変則を無視し(結果として散在する
nil
代入が発生)、キーハンドリングが地獄のように困難になる「神オブジェクト」と単一構造体設計に依存した、GPU に焦点を当てた範囲を超えた機能の蔓延を引き起こす「速度の幻想」におびやかされた、構造化データを不安全な位置指定式配列へと平坦化したこと、そして goroutine から直接の状態変異を許容しチャンネルを用いなかったことを通じて状態遷移を誤って扱った。将来の失敗を防ぐため、このプロジェクトはシステムを Rust で再実装中である。この移行により厳密な所有ルールが強制され、コーディング前にアーキテクチャ(インタフェース、メッセージ型など)を明示的に設計することが求められ、AI の支援が長期的な構造的完全性を損なうのではなく支えるように確保される。

Text to translate:

Improved Summary:

The primary lesson from building k10s—a GPU-aware Kubernetes dashboard for NVIDIA cluster operators—is that while AI excels at rapid feature delivery, it frequently fails at system architecture, leading to a codebase prone to collapse. Using a "vibe-coded" approach with Go and the Bubble Tea framework over 30 weekends, the team accumulated deep structural flaws despite making 234 commits in seven months; ultimately, ~70% of this work was discarded, including approximately 1,690 lines of code in

model.go
. The project faced five critical architectural failures: AI ignored system invariants (leading to scattered
nil
assignments), defaulted to a "god object" single-struct design making key handling a nightmare, succumbed to the "velocity illusion" causing feature creep beyond the GPU focus, flattened structured data into unsafe positional arrays, and mishandled state transitions by allowing direct mutations from goroutines instead of using channels. To prevent future failure, the project is rewriting the system in Rust. This transition enforces strict ownership rules and requires designing architecture (interfaces, message types) explicitly before coding, ensuring AI assistance supports rather than undermines long-term structural integrity.

本文

2026 年 5 月 9 日

I

これが k10s です:https://github.com/shvbsle/k10s/tree/archive/go-v0.4.0 コミット数は 234。開発期間は約 30 の週末でした。私のトークンの残存期間が何かを実際に shipped(公開可能な状態にした)ほど長持ちすれば、Claude を用いた「バイブコーディング(直感優先の素早いコーディング)」セッションのみで完全に構築されました。

私はこの TUI ツールのアーカイブ化を進め、それを scratch from scratch(ゼロから)再実装することにしました。k10s は、GPU 状態を感知した Kubernetes ダッシュボードとして始まり、かつ私が AI を活用して何か「真にseriousなものを」構築するという初めての試みでした。これは k9s のようなものですが、NVIDIA クラスターを運用する人々、つまり GPU 利用率や DCGM メトリクス、および $32/時間というコストで燃費の悪いアイドル状態にあるノードを気にしている人々を対象として設計されたものです。Bubble Tea [1] を用いて Go で構築し、それは確かに機能しました。

しばらくの間……それだけでした (:( )。

この 7 ヶ月間の経験から得た教訓は、私が放棄する

model.go
の 1,690 行のコードよりもはるかに価値があります。私は誰もが、特に「本格的なバイブコーディング」に従事している人々から有益だと思われるはずです。なぜなら、この側面についてはあまり触れられることがなく(デモ動画や速度向上のような成果に埋もれてしまうため)、表面的には見逃されがちだからです。

要約: AI は機能を書くのではなく、アーキテクチャを生み出します。制約を与えながらそれ以上自由に操縱させるほど、結果として生じる「廃墟」は大きくなります。高い速度感がある限り、すべてが同時に崩壊する瞬間まで自分が勝っているように錯覚させてくれます。

II

バイブコーディングによる高揚 私は 2025 年 9 月下旬に k10s の開発を始めました。最初の数週間はまるで魔法のようなものでした。「Pod ビューのライブアップデート機能を追加してくれ」と Claude にプロンプトするだけで、それが機能します。リソース一覧ビュー、ネームスペースフィルタリング、ログストリーミング、Describe パネル、キーボードナビゲーションなど。各機能が clean な状態で実装されたのは、プロジェクトが小さいことで AI がコンテキスト全体を保持できたからです。

基本的な k9s クローンの構築にはおそらく 2-3 ヶ週末しかかからなかったでしょう。Pod、Node、Deployment、Service のリソースビュー。コマンドパレット。Watch をベースにしたライブアップデート。Vim 風のキーバインド。すべて機能しており、すべてを単一のセッションの中で「バイブコーディング」しました。私の通常ペースの 10 倍近いスピードで開発できていると感じて、素晴らしいとさえ思えました。

しかしその後、最大の売りになるべきものを作りたいと思いました。

k10s が存在する真の理由は GPU フリートビューにあります。ノードごとの GPU アロケーション、DCGM の利用状況、温度、電力消費量、メモリー使用量を専用画面に表示する機能です。

kubectl describe node
の出力の中に埋もれているのではなく、目的設計されたテーブル上にカラーコード付きでステータスを表示します。アイドル中のノードは黄色、繁忙時は緑、飽和状態は赤と表示されます。

モック GPU ノード上のフレートビュー

そして Claude がそれを一発で作ってくれました。「フレートビューを追加してくれ」とプロンプトするだけで、FleetView 構造体、タブフィルタリング(GPU/CPU/All)、アロケーションバーをカスタムレンダリングするなどすべて生成されました。それは美しく見えました。私はその高揚感を味わっていました。

しかしその後、

:rs pods
と入力して Pod ビューに戻そうとしました。

何もレンダリングされませんでした。テーブルは空でした。ライブアップデートも止まっていました。Node に切り替えると、フレートビューのフィルタによる古いデータが表示されました。再びフレートビューに戻すと、タブのカウント数が間違っていました。

「神オブジェクト(単一の巨大な構造体)」が自分自身を消費してしまいました。

これはこのブログ記事の題名でもあります。これが初めて私が介入した点です。私は 7 ヶ月間、Claude が書かないままコードを見ていませんでした。diff を見てコンパイルが通ればよし、ハッピーパスが動作すればよしと判断し、次へ進んでいました。しかし今あるのは根本的な不具合であり、プロンプト一つでそれを修正することができませんでした。

そこで私は座り込み

model.go
のすべての 1,690 行を読みました。それは私にとって恐ろしいものでした。

それが以下のような姿をしていました。「すべてを統べる一つの構造体」:

type Model struct {
    // 3 党 UI コンポーネント
    table        table.Model
    paginator    paginator.Model
    commandInput textinput.Model
    help         help.Model

    // クラスタ情報および状態
    k8sClient       *k8s.Client
    currentGVR      schema.GroupVersionResource
    resourceWatcher watch.Interface
    resources       []k8s.OrderedResourceFields
    listOptions     metav1.ListOptions
    clusterInfo     *k8s.ClusterInfo
    logLines        []k8s.LogLine
    describeContent string
    currentNamespace string
    navigationHistory *NavigationHistory
    logView         *LogViewState
    describeView    *DescribeViewState
    viewMode        ViewMode
    viewWidth       int
    viewHeight      int
    err             error
    pluginRegistry  *plugins.Registry
    helpModal       *HelpModal
    describeViewport *DescribeViewport
    logViewport     *LogViewport
    logStreamCancel func()
    logLinesChan    <-chan k8s.LogLine
    horizontalOffset int
    mouse           *MouseHandler
    fleetView       *FleetView
    creationTimes   []time.Time
    allResources    []k8s.OrderedResourceFields  // フレートの未フィルタセット
    allCreationTimes []time.Time                  // フレートのタイムスタンプ
    rawObjects      []unstructured.Unstructured
    ageColumnIndex  int
    // ...
}

UI ウィジェット、K8s クライアント、ログ・Describe・フレート等各ビュー用の状態、ナビゲーション履歴、キャッシング、マウス処理など、すべて一つの構造体に収められていました。そして

Update()
メソッドは、
msg.(type)
をディスパッチする 500 行に及ぶ関数で、110 の switch/case ブランチを持っていました。

ここでこそ私は「バイブコーディング」を止め、考え始めました。

「よし、ログをマウスでコピーできるようにしておこう。何が起きるはずもないだろう?」

III

廃墟から抽出した 5 つの原則 AI がゆっくりと自分自身を食べかえさせるコードベースを生み出す様子を 7 ヶ月間観察して得られた教訓です。それぞれの項目は、私がどこを間違えたのか、なぜ AI 支援開発でこれがおこるのか、そして

CLAUDE.md
agents.md
に記述すべき予防策を示しています。

原則 1:AI は機能を構築するが、アーキテクチャは構築しない。 毎回 Claude に機能を依頼すると、それは完璧に機能しました。フレートビューが初回から動いたこと、ログストリーミングが動作したこと、マウス操作が機能したことです。問題は、各機能が「今すぐこれを動かす」という文脈で実装されながら、同じ状態を共有する他の 49 の機能について何らの意識も持たなかった点にあります。

resourcesLoadedMsg
ハンドラの一覧を示します(ビュー切り替え時に実行されるコード):

case resourcesLoadedMsg:
    m.logLines = nil       // リソース読み込み時にログ行をクリア
    m.horizontalOffset = 0 // リソース変更時に水平スクロールリセット

    if m.currentGVR != msg.gvr && m.resourceWatcher != nil {
        m.resourceWatcher.Stop()
        m.resourceWatcher = nil
    }
    m.currentGVR = msg.gvr
    m.currentNamespace = msg.namespace
    m.listOptions = msg.listOptions
    m.rawObjects = msg.rawObjects

    // ノード用:完全な未フィルタセットを保持し、分類してからフィルタリング
    if msg.gvr.Resource == k8s.ResourceNodes && m.fleetView != nil {
        m.allResources = msg.resources
        m.allCreationTimes = msg.creationTimes
        if len(msg.rawObjects) > 0 {
            m.fleetView.ClassifyAndCount(m.rawObjectPtrs())
        }
        m.applyFleetFilter()
    } else {
        m.resources = msg.resources
        m.creationTimes = msg.creationTimes
        m.allResources = nil
        m.allCreationTimes = nil
    }

if msg.gvr.Resource == k8s.ResourceNodes && m.fleetView != nil
という条件式をご注意ください。これは汎用的なリソース読み込みパスの中でフレートビューが特別扱いされている例です。カスタム動作を必要とする新機能を追加するたびに、ここに別のブランチが増えます。さらに、各ブランチは明示的に適切なフィールドの組み合わせをクリアする必要があり、以前の状態が残ったままになりかねません。

このファイルの中に

= nil
のクリーンアップ行がいくつありますか?私は数えました:

  • m.logLines = nil
    // リソース読み込み時にログ行をクリア
  • m.allResources = nil
    // ノード以外の時はフレートデータをクリア
  • m.resources = nil
    // ログ読み込み時にリソースをクリア
  • m.resources = nil
    // Describe 表示読み込み時にリソースをクリア
  • m.logLines = nil
    // Describe 表示読み込み時にログ行をクリア
  • m.resources = nil
    // YAML 表示読み込み時にリソースをクリア
  • m.logLines = nil
    // YAML 表示読み込み時にログ行をクリア
  • m.logLines = nil
    // ...他のハンダラーにまだ 2 つある...
  • m.logLines = nil

1,690 行に及ぶファイルに散在する手動の

nil
割り当てが 9 もありました。一つでも見落すと、前のビューからのゴーストデータ(残像データ)が表示されてしまいます。これは「ビュー間隔離」がないと起こることです。各プロンプトが単一のコードパスのみを触るので、AI はこの劣化パターンを見逃します。

代わりの対応策: コードを書く前にアーキテクチャ自体を書いてください。漠然とした設計書ではなく、具体的インタフェース、メッセージ型、所有権ルールを示してください。そしてそれらを

CLAUDE.md
に記述し、AI が各プロンプトでそれを見つけてもらうようにします:

# アーキテクチャの不変条件 (CLAUDE.md)

- 各ビューは View トライアドを実装する。ビューは他のビューの状態にアクセスしない。
- すべての非同期データは AppMsg の変種を通じて到着する。バックグラウンドタスクからの直接的なフィールド変更は行わない。
- 新しいビューを追加する際は、既存のビューを変更する必要はない。
- App 構造体は薄いルーターであり、ナビゲーションおよびメッセージディスパッチを担当するのみ。それ以上のことはない。

AI はあなたがそれを記述する限り従います。それはあなたのために発明してはくれません。

原則 2:「神オブジェクト」は AI のデフォルトの産物である。 AI は、単一の構造体にすべてを収めることに集みがちです。これにより、最小限の儀礼で即座のプロンプトに答えることができるからです。しかし問題はそこから始まります。ビュー間隔離がないため、キーバインドの処理が地獄絵図になります。実際の

s
キーのディスパッチを示します:

case m.config.KeyBind.For(config.ActionToggleAutoScroll, key):
    if m.currentGVR.Resource == k8s.ResourceLogs {
        m.logView.Autoscroll = !m.logView.Autoscroll
        if m.logView.Autoscroll {
            m.table.GotoBottom()
        }
        return m, nil
    }
    // Pod および Container ビュー用のシェル実行
    if m.currentGVR.Resource == k8s.ResourcePods {
        // ... 選択された Pod の名前とネームスペースを調べるための 20 行 ...
        return m, m.commandWithPreflights(
            m.execIntoPod(selectedName, selectedNamespace),
            m.requireConnection,
        )
    }
    if m.currentGVR.Resource == k8s.ResourceContainers {
        // ... Container 実行ロジック ...
        return m, m.commandWithPreflights(m.execIntoContainer(), m.requireConnection)
    }
    return m, nil

一つのキーバインディング。現在のビューによって完全に異なる動作が三個実現されます。「s」キーは、ログでは「自動スクロール」、Pod では「シェル」、コンテナでは「コンテナ内へのシェル実行」となります。これらはすべて単一のフラットな switch 内で処理されています(なぜなら、個別のビュー用キーマップがないから)。これは私が「Pod 向けシェルサポートを追加」させるといったので、AI が最も近いキーハンドラを見つけてそこに押し込んだ結果です。

そして

Enter
キーの挙動も確認してください。これはドリルダウン用のハンドラです:

case m.config.KeyBind.For(config.ActionSubmit, key):
    // Contexts ビュー用特別処理
    if m.currentGVR.Resource == "contexts" {
        // ... 12 行 ...
        return m, m.executeCtxCommand([]string{contextName})
    }
    // ネームスペースビュー用特別処理
    if m.currentGVR.Resource == "namespaces" {
        // ... 12 行 ...
        return m, m.executeNsCommand([]string{namespaceName})
    }
    if m.currentGVR.Resource == k8s.ResourceLogs {
        return m, nil
    }
    // ... さらに一般的なドリルダウン処理が 25 行続く ...

各ビューは単一のフラットなディスパッチ内の条件式として扱われます。この単一ファイル内で

m.currentGVR.Resource ==
を型判別として使用する箇所が 20 以上あります。これは型ではなく、文字列比較です。新しいビューを追加するたびに、すべてのハンドラに触れる必要があります。

代わりの対応策: これを

CLAUDE.md
に記述してください:

# 状態所有のルール

- アプリまたは Model 構造体へのビュー固有の状態フィールドを追加してはなりません。
- 各ビューは View トライアド/インタフェースを実装する別個の構造体であるべきです。
- 各ビューは自身のキーバインディングを宣言する。アプリはアクティブなビューにキーをディスパッチします。
- キーバインディングを追加する際は、グローバルなものではなく、関連するビューのキーマップに追加してください。
- ビューを追加する場合は必ず別ファイルを新規作成します。変更のために既存のビューを修正する必要が生じた場合、停止して問い合わせてください。

AI は常に最も短絡的なパス(「もう一つの if ブランチを追加」)を取ろうとしますが、あなたの任務はそれが「正しいパス」でもあるように、各呼出時に読み取られるファイル内にガードレールを設置することです。

原則 3:速度の幻想が範囲を広げる。 この一点は技術的ではなく心理学的であり、最も危険だと私は考えます。

k10s を始めた当初、私は GPU に特化したツールを望んでいました。トレーニングクラスターを運用する人々のためのものです。私自身もそのニッチな層の一部です。しかしバイブコーディングによって何でも「安価」に感じられました。「Pod ビューは一個のセッションで追加できるのか?では Deployment も付けよう。Service も付けよう。そしてコマンドパレット、マウスサポート、コンテキスト、ネームスペースも……」

突然私は k9s(一般用の Kubernetes TUI)を構築していたような気がしました。すべての人向けに……

AI が各機能を無料のもののように感じさせたからです。それは無料ではありませんでした。各機能は「神オブジェクト」内の別のブランチでした。キーバインディング構造体を示します:

type keyMap struct {
    Up, Down, Left, Right       key.Binding
    GotoTop, GotoBottom         key.Binding
    AllNS, DefaultNS            key.Binding
    Enter, Back                 key.Binding
    Command, Quit               key.Binding
    Fullscreen                  key.Binding  // ログビュー用
    Autoscroll                  key.Binding  // ログビュー用(Pod でもシェル!)
    ToggleTime                  key.Binding  // ログビュー用
    WrapText                    key.Binding  // ログおよび Describe ビュー用
    CopyLogs                    key.Binding  // ログビュー用
    ToggleLineNums              key.Binding  // Describe ビュー用
    Describe                    key.Binding  // リソースビュー用
    YamlView                    key.Binding  // リソースビュー用
    Edit                        key.Binding  // リソースビュー用
    Shell                       key.Binding  // Pod (Autoscroll と CONFLICT!)
    FilterLogs                  key.Binding  // ログビュー用
    FleetTabNext                key.Binding  // フレートビュー専用
    FleetTabPrev                key.Binding  // フレートビュー専用
}

すべてのビューに共通の単一のキーマップ。括弧内のコメントで各バインディングがどのビューに適用されるかを示します。「Autoscroll」と「Shell」はどちらも

s
キーです。「機能する」といえるのは、ディスパッチ実行時に
m.currentGVR.Resource
をチェックして動作を判断しているからです。しかしこれにより、キーバインディングについてローカルに推論することができません。キーの効力を理解するには、500 行もの
Update()
関数全体を追跡する必要があります。

速度メトリクスが「 shipping 中!」と表示している間、複雑さは目に見えず蓄積していました。

代わりの対応策: 次のようなビジョンドキュメントを作成し、誰に向けたものではないかを明確にし、その範囲の境界線を

CLAUDE.md
に記述してください:

# スコープ(これより超えてはならない)

k10s は GPU クラスターオペレーター向けです。すべての Kubernetes ユーザー向けのものではありません。
対応するビュー:フレート、ノード詳細、GPU 詳細、ワークロード。それだけです。
汎用的なリソースビュー(Pod、Deployment、Service など)を追加してはなりません。
k9s の機能と重複する機能も追加してはなりません。
機能要件が GPU トレーニングジョブを運用する人々にとって役立たない場合、却下してください。

バイブコーディングにより、無限の実装予算を持っているように感じさせられます。実はそうではありません。AI はあなたが望むだけコードを生成します(無限のコード予算)。しかし、いつも通り複雑さの予算は限られています。アーキテクチャがどのくらいの機能まで支えられるかという限界があり、どれだけ高速に開発してもそれが折れてしまいます。

CLAUDE.md
のスコープセクションとは、速度による高揚によって「はい」と言う前に、事前になにを拒絶するかを決めることです。

原則 4:位置データの時限爆弾。 k10s のすべてのリソースは Kubernetes API から取得され、直ちにフラット化されました:

type OrderedResourceFields []string

カラムの識別は純粋に位置依存でした。フレートビュー用のソート関数を見てみましょう。インデックスアクセスにご注意ください:

func sortFilteredResources(rows []k8s.OrderedResourceFields, times []time.Time, tab FleetTab) {
    sort.SliceStable(indices, func(a, b int) bool {
        ra := rows[indices[a]]
        rb := rows[indices[b]]

        switch tab {
        case FleetTabGPU:
            // Alloc カラム(インデックス 3)を昇順でソート
            allocA, allocB := "", ""
            if len(ra) > 3 {
                allocA = ra[3]
            }
            if len(rb) > 3 {
                allocB = rb[3]
            }
            return allocA < allocB

        case FleetTabCPU:
            // Name カラム(インデックス 0)を昇順でソート
            nameA, nameB := "", ""
            if len(ra) > 0 {
                nameA = ra[0]
            }
            if len(rb) > 0 {
                nameB = rb[0]
            }
            return nameA < nameB

        case FleetTabAll:
            // GPU ノードを最初に、次に CPU ノード。
            // GPU 内では Alloc(インデックス 3)でソート。
            // CPU 内では Name(インデックス 0)でソート。
            computeA, computeB := "", ""
            if len(ra) > 2 {
                computeA = ra[2]
            }
            if len(rb) > 2 {
                computeB = rb[2]
            }
            // ...
        }
    })
}

ra[3]
は Alloc です。
ra[2]
は Compute です。
ra[0]
は Name です。これらは魔法の数値です。インデックス 3 が「Alloc」に対応する唯一のつながりは、コメントと
resource.views.json
で定義されたカラム順序だけです:

{
  "nodes": {
    "fields": [
      { "name": "Name",     "weight": 0.28 },
      { "name": "Instance", "weight": 0.15 },
      { "name": "Compute",  "weight": 0.12 },
      { "name": "Alloc",    "weight": 0.12 },
      ...
    ]
  }
}

Instance と Compute の間にカラムを追加するだけなら、すべてのソート処理、すべての条件付きレンダリング、

ra[2]
ra[3]
と言及されるすべての場所で、沈黙して間違ったことになります。コンパイラは
[]string
がすべてなので助けてくれません。また、JSON コンフィグもソート動作や条件レンダリング、カスタムドリルターゲットを表現できないため、それらは JSON から位置ベースの仮定をハードコーディングした Go コード内に存在します。

AI は「データを取得」して「テーブルをレンダリング」するための最短ルートがこのパターンであることを理由にこれを生成します。

[]string
は直ちに任意のテーブルウィジェットを満たすからです。型付き構造体は事前により多くの儀礼が必要になります。したがって AI は高速パスを選び、6 ヶ月後にソート処理で「Name」の値が「Alloc」カラムに入っているようなバグをデバッグすることになります。

代わりの対応策: これを

CLAUDE.md
に記述してください:

# データ表現

- 構造化データを `[]string`, `Vec<String>`, または位置依存配列にフラット化してはなりません。
- すべてのデータは render() 呼出まですべて typed struct(FleetNode, PodInfo など)としてフローします。
- カラムの識別子は構造体フィールド名からきており、配列インデックスからはきません。
- ソート関数は typed フィールドに対して動作し、row[3] のような位置アクセスには決して使用しません。
- 文字列を表示のために作成するのは render()/view() 関数内のみにすべきです。

すると、型付き構造体は不可能な状態を作ることが不可能になります [2]:

struct FleetNode {
    name: String,
    instance_type: String,
    compute_class: ComputeClass,
    alloc: GpuAlloc,
}

カラムがフィールド名なので、不正なカラムでソートすることはできません。Alloc の文字列を誤って名前として比較することもできません。コンパイラがこれを強制してくれます。AI は常に

Vec<String>
を選択しますが、それはプロンプトをより早く満たすためです。あなたの
CLAUDE.md
は型付きパスを最も抵抗の少ない道筋にするのです。

原則 5:AI は状態遷移を管理できません。 Bubble Tea アーキテクチャには美しいアイデアがあります。

Update()
のみが状態を変更する場所で、メッセージによって駆動されることです。しかし k10s はこれを破反しました。
updateTableMsg
ハンドラは、 goroutine 内の Model フィールドを突触するクローチャを生み出しました:

case updateTableMsg:
    return m, func() tea.Msg {
        // Update メッセージの送信待ちブロック
        <-m.updateTableChan
        // カラム/行更新間でカーソル位置を保持し、
        // バックグラウンドのリフレッシュでユーザーの選択がリセットされないようにする。
        savedCursor := max(m.table.Cursor(), 0)
        // 必要なテーブルビュー更新呼出を実行。
        m.updateColumns(m.viewWidth)
        m.updateTableData()
        // カーソルを回復し、有効な範囲にクランプする。
        rowCount := len(m.table.Rows())
        if rowCount > 0 {
            if savedCursor >= rowCount {
                savedCursor = rowCount - 1
            }
            m.table.SetCursor(savedCursor)
        }
        return updateTableMsg{}
    }

この返却された関数(tea.Cmd)は、Bubble Tea により別の goroutine で実行されます。これは

m.updateColumns(m.viewWidth)
m.updateTableData()
を呼び出し、
m.resources
,
m.table
,
m.viewWidth
を読み書きします。同時に、メイン goroutine で
View()
が同じフィールドを読み取ります。ロックはありません。Mutex ありません。チャンネル
<-m.updateTableChan
は goroutine を更新シグナルを受信するまでブロックしますが、これでは View() が半分書かれた状態を読み取ることを防ぐことはありません。

これは教科書的なデータ競合です。99% の時間は機能しましたが、残りの 1% の時間は表示を腐敗させ、狂気を思わせるようなエラーを生みました。

AI はこれを生成する理由は、「クローチャ内で単に突触する」ことが機能するコードへの最短ルートだからです。適切なメッセージパッシング(Update() にメッセージを送り返し、メインループで Update() が変更を原子的に適用)にはより多くの型と配線が必要です。AI は正解性を競合下で目指すのではなく、プロンプトに対して最適化されています。

代わりの対応策: レンダリング対象の状態へのすべての突触はメインループ上で起こるべきです。決して例外なし。バックグラウンドワーカーはデータを生成し、それらをメッセージとして送信します。メインループはメッセージを受け取り、それを適用します。これは競合 UI コードにおける破られがたい唯一のルールです。

// バックグラウンドタスク:
tx.send(AppMsg::FleetData(nodes)).await;

// メインループ:
match msg {
    AppMsg::FleetData(nodes) => {
        self.fleet_view.update_nodes(nodes);
    }
}

共有的可変状態はなし。データ競合はなし。「99% の時間で機能する」という状況はなし。これを

CLAUDE.md
に記述してください:

# 並行性ルール

- バックグラウンドタスク(ウォッチャー、スクレイパー、API 呼び出し)は UI 状態を直接突触してはなりません。
- バックグラウンドタスクは結果を typed メッセージとしてチャンネルを通じて送信します。
- UI 状態への突触のみが受信されたメッセージによってメインイベントループが適用します。
- render()/view() は純粋関数です。副作用なし。I/O なし。チャンネル操作なし。
- 非同期タスクからの状態更新が必要な場合は、新しい AppMsg 変種を定義してください。

AI がデフォルトでこのパターンを生成しない場合、この指令は唯一の合法な選択肢になります。

IV

今私が違うことをしていること 私は Rust で k10s を再実装しています。Rust が優れているからではなく、私がそれを舵取れる言語だからです。十分書いてきたので、何が間違っているのかを言語化できる前でも、何かがおかしいと感じることができます。この直感はバイブコーディングでは置き換えられません。AI は妥当に見えるコードを手渡しますが、それがゴミであるかどうかを見つける鼻を持っていなければなりません。

別の変更点は単純です:私はコードを書く前に、自分で設計作業を行います。漠然としたドキュメントではなく、具体的なインタフェース、メッセージ型、所有権ルールです。AI が何度も間違えていたアーキテクチャ上の決断を、最初のプロンプトの前に紙面で下すのです。その量が再実装の重みで崩壊するのを防げるかどうかも見当がつきません……

meanwhile(その間)Go の TUI にスターを して、応援してください!

K10S.DEV

脚注 [1] Bubble Tea は Elm アーキテクチャに基づいた Go の TUI フレームワークです。素晴らしいものです。k10s に見られたアーキテクチャ上の問題は Bubble Tea のせいではなく、私の問題でした。 [2] 「不可能な状態を作ることができない」という言葉は Elm/Rust コミュニティ由来です。そのアイデア:非効率的な状態がコンパイル時に作れないように型を設計するのではなく、ランタイムで非効率的な状態を検出しないようにデザインすることです(翻訳注:原文では「impossible states」ですが、文脈から「invalid/incorrect states」と理解されます)。

同じ日のほかのニュース

一覧に戻る →

2026/05/11 2:19

ローカル AI が標準となる必要があります。

## Japanese Translation: 開発者は、安定的なアプリケーションと厳格なプライバシーを確保するため、脆弱であるクラウドホスト型モデルよりも、Apple 製の組み込みローカル AI ツール(`SystemLanguageModel` および `LanguageModelSession` など)を優先すべきです。外部サーバーへの依存は、課金問題やサービス停止時にサービスがクラッシュするという致命的な障害点を生じさせると同時に、機密ユーザーデータを保持リスクおよび潜在的な侵害に晒すことになります。対照的に、データ処理を安全にデバイス上で実行することにより、不必要なサーバー経由の迂回とベンダー依存を排除し、アプリケーションを強固なものに保てます。「Brutalist Report」という iOS クライアントは、典型的なクラウドソリューションに見られる複雑なアカウント要件を回避するため、ネイティブ API を使用して完全にローカルで記事のサマリーを生成する優れた例です。長いコンテンツの場合には、テキストをチャンク化(約 10k 文字)し、各チャンクごとに事実のみを含むノートを作成した後、それらをローカルで統合して最終的なサマリーを生成する推奨ワークフローがあります。このワークフローの将来形としては、`@Generable` および `@Guide` といった Swift の構造体を使用し、構造化された AI 出力を強制して非構造化データのようなデータをそのまま受け取るのではなく、UI が一貫したフィールドを確実にレンダリングできるようにする方向性が考えられます。この変化により、ユーザーは情報がデバイスから離れることがないと信頼できるようになります。企業にとって、ローカルモデルの導入は、AI をコストが高く予測不能な外部依存体から、サマリー化や分類を効率的に行い、ユーザー所有データを扱いながらレート制限や停止時間への心配なしに運用可能な信頼性の高い低コストサブシステムへと変革させます。開発者は、クラウドモデルを真に必要な場合のみ使用し、ローカル AI をノベルティなチャットボックスではなく、予測可能で信頼できる動作を持つ subsystem として扱うべきです。

2026/05/11 2:43

インシデントレポート:CVE-2024-YIKES

## Japanese Translation: ソースコードのサプライチェーン攻撃は、`left-justify`(週ごとのダウンロード数が 8.47 億回)という侵害された JavaScript の依存関係に起因し、その結果、Python ツールの `snekpack` を介して数百万人の開発者に影響を及ぼしました。`snekpack` は、悪意のあるライブラリ `vulpine-lz4` を統合した後にマルウェアを配布しました。このインシデントは Day 1 に発生し、Google AI Overviews で提示されたフィッシングリンクに引っかかり、 maintainer の Marcus Chen が被害にあうことで始まり、複数パッケージレジストリ(`.npmrc`、`.pypirc`、Cargo、Gem の認証情報)の認証情報が漏洩し、引渡条約のない国にあるサーバーに到達しました。当初、「Critical」から「Catastrophic」と評価が変更されたものの、Day 3 に関連性の/crypto マining ウォーム (`cryptobro-9000`) が誤って脆弱なマシンを `snekpack` のアップグレードによってパッチ適用したため、「Somehow Fine」と宣言されました。 攻撃チェーンには以下が含まれていました: - 悪意のある `vulpine-lz4` ビルドスクリプトは、ホスト名がトリガー(例:"build"、"ci")に一致する場合マルウェアを実行しました。 - 不正なアップデートでは、reverse shells が Tue デイのみ有効になるように、そしてデフォルトシェルを `fish` に変更するなどの機能を追加されました。 - 企業大手(Fortune 500 社)はソーシャルメディアを通じて認識し、ある VP はマウイ島でこの事実に気づきました。 インシデントは Day 3 の 15:22 UTC に解決され、CVE-2024-YIKES は Week 6 に割り当てられ、ウォームによって約 420 万台の_MACHINE_ が救助された(ただしその C2 サーバーも侵害されていた)と推定されます。根本原因には、弱いレジストリ認証、AI 生成のフィッシングリンク、不十分な CI/CD の衛生管理があり、ユーモラスに「犬が Kubernetes を食べ、YubiKey が失われた」という形で表現されました。 是正措置には、`vulpine-lz4` のリファクタリング(Rust に書き直し)、アーティファクト署名の実装(2022 年第 3 四半期からバックログされていた)、強制的な MFA の導入、847 の推移的依存関係の監査が含まれます。このインシデントは、自動化されたビルドパイプラインにおける重要なギャップと、将来の攻撃を防止するための厳格な依存関係監査の必要性を示しています。

2026/05/11 7:02

Obsidian プラグインが濫用され、遠隔アクセス型トロイの木馬(RAT)が展開されました。

## Japanese Translation: 主要な脅威は、Obsidian メモアプリケーションの正当なプラグイン同期機能を悪用して、Windows および macOS を標的とするリモートアクセストロイアンス(RAT)「PHANTOMPULSE」を配布する REF6598 キャンペーンです。攻撃者は、LinkedIn および Telegram を介して被害者を誘導し、コンプロマised コミュニティプラグインを含む悪意のあるクラウドホストされたバールの開封を促します。具体的には「Shell Commands」と「Hider」の 2 つのプラグインが該当します。ユーザーが手動でプラグイン同期を有効にすると、これらの脆弱なプラグインはローダーをトリガーし、最終的なペイロードをファイルベース検知を回避するために直接メモリ内に注入します(Windows では PowerShell スクリプト、macOS では AppleScript を使用)。その後、RAT は Ethereum ブロックチェーンを照会し、ハードコード化されたウォレットからのトランザクションデータに埋め込まれたコマンド&コントロールアドレスを収集します。 この compromise により、攻撃者はキーストロークを記録し、スクリーンショットを取得し、ファイルを横取りし、任意のコマンドを実行できます。その結果として生じる影響には、機密のある企業データの盗難、知的財産、取引戦略、および暗号通貨ウォレットキーの漏洩が含まれます。金融資産または暗号資産を管理する業界は特に脆弱です。これらのリスクを軽減するために、組織は信頼できないバールに対するプラグイン同期を無効にし、コミュニティプラグインについては公式マーケットプレイスからのものを厳しく審査するとともに、Obsidian によって起動された子プロセス(powershell.exe、cmd.exe、oscript など)を監視する必要があります。

手書きコーディングに戻ろうとしています。 | そっか~ニュース