REVELUP
shwld
shwld
作ることに関わるひとを幸福にしたい スキ: 個人開発 設計 アジャイル 関数型 DDD

Notes

Friend only
This note is available to friends only.
17 days ago
Sign in to read

子プロセス用 pipe は exec 前に不要 FD を閉じる

CLI ラッパーから別プロセスへ prompt を stdin で渡す処理が、子プロセスの終了待ちで必ず timeout した。終了監視には waitpid を使っていたが、原因は監視ではなく pipe の file descriptor(FD)継承だった。

親が prompt を書き終えて stdin の書き込み側 FD を閉じても、子が同じ書き込み側 FD を継承していると EOF が届かない。pipe は書き込み側 FD が 1 つでも開いている間、読み込み側へ「これ以上データが来ない」と通知できないためだ。

親: stdin_write を閉じる
子: stdin_write をまだ保持している
  ↓
子が stdin の EOF を待ち続ける
  ↓
子が終了しない
  ↓
waitpid は正常に「未終了」と判定する

OCaml の Unix.pipe は、cloexec を省略すると歴史的理由で keep-on-exec になる。公式ドキュメントでも安全なデフォルトではないため明示指定が推奨されている。

let stdin_read, stdin_write = Unix.pipe ~cloexec:true () in
let stdout_read, stdout_write = Unix.pipe ~cloexec:true () in
let stderr_read, stderr_write = Unix.pipe ~cloexec:true () in

close-on-exec にすると、fork 後に別プログラムへ exec する時点で不要な FD が閉じられる。親が prompt を書き終えて閉じれば、子へ EOF が届いて正常終了できる。

回帰テストでは、stdin を EOF まで読む偽 CLI を使う。stdin を読まない偽 CLI では FD リークを再現できない。

cat >/dev/null
printf '%s\n' '{"summary":"Summary","items":[]}'

終了待ちが timeout したとき、waitpid の置き換えやプロセス名ポーリングを足す前に、子が終了可能な FD 状態で起動されているかを確認する。

参考: OCaml Unix API

17 days ago
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read

lefthookでbun installの実行チェックをpre-commitに入れる

bun.lock が更新されたのに bun install を実行していないままコミットしようとすると、CIや他の開発者の環境で依存関係がズレる。lefthook の pre-commit に依存関係チェックを入れることで、このミスをコミット前に防ぐ。

Before: bun install を忘れてコミットしても気づけない。 After: node_modules/.bin のタイムスタンプと bun.lock を比較し、古ければコミットをブロックする。

# lefthook.yml
assert_lefthook_installed: true

pre-commit:
  commands:
    deps-check:
      run: |
        if [ ! -d node_modules/.bin ] || [ bun.lock -nt node_modules/.bin ]; then
          echo "node_modules が古いです。bun install を実行してください。"
          exit 1
        fi

設計のポイント:

  1. assert_lefthook_installed: true — lefthook バイナリが見つからない場合に hook 実行自体を exit 1 で失敗させる公式オプション。新規参加者がインストールを忘れても気づけるようになる
  2. node_modules/.bin をタイムスタンプ比較の対象にする — bun install はパッケージに変更がない場合でも .bin のタイムスタンプを更新する(公式ドキュメントに明記はないが実装の観察値)。一方 node_modules ディレクトリ直下は変更がなければタイムスタンプが変わらないことがあり、「install 済みなのに古いと判定される」フォールスポジティブが起きた。.bin を対象にすることでこの問題を回避できる
  3. -nt(newer than)演算子 — bash の [[ A -nt B ]] で A が B より新しいか判定できる。bun.lock -nt node_modules/.bin は「lockfileの方が新しい=install未実行」を意味する

lefthook の setup フックは staged ファイルがない状態では skip されることがあるため、commands の先頭に deps-check を入れる方が確実にブロックできた。

a month ago

Tailwind Typography の prose-code: は pre 内の code にも適用される

Tailwind Typography で prose-code:text-[var(--color-text)] のようなカスタム色を設定すると、インラインコード(`code`)だけでなく、コードブロック(```...```)内の <code> にも同じ色が適用される。

コードブロックの背景は Tailwind Typography のデフォルトで暗い色(slate-800 相当)になっているため、そこに濃い文字色を上書きすると「暗い背景に暗い文字」になってコードが読めなくなる。

<!-- この class は pre > code にも効いてしまう -->
<div class="prose prose-code:text-[var(--color-text)]">
  <!-- インラインコード -->
  <p>Use <code>useState</code> here.</p>

  <!-- コードブロック(背景が暗いのに文字も暗くなる)-->
  <pre><code>const x = 1;</code></pre>
</div>

修正は pre 内の code だけを明示的に上書きする:

<!-- [&_pre_code]: でスコープを絞る -->
<div class="prose prose-code:text-[var(--color-text)] [&_pre_code]:text-slate-100">

または、インラインコードだけに色を当てたい場合は :not(pre) で絞る:

<div class="prose [&_:not(pre)_code]:text-[var(--color-text)]">

prose-code: はすべての code 要素を対象にするセレクタで、Tailwind Typography が内部で生成する --tw-prose-code 変数を上書きする。この変数は pre > code にも引き継がれるため、意図せずコードブロックにも効いてしまう。インラインコードとコードブロックの色を分けたい場合は、最初から :not(pre) のアービトラリーセレクタで限定する方が安全。

a month ago

progenitor で OpenAPI spec から Rust クライアントをコンパイル時生成

Rust CLI から HTTP API を呼び出すとき、手書きの reqwest クライアントではなく progenitor crate でコンパイル時にクライアントコードを自動生成する方法を試した。

OpenAPI の JSON ファイルをリポジトリに保存しておき、build.rs から progenitor を呼ぶと、ビルド時に型付きクライアントが生成される。サーバ側の API 定義が変われば JSON ファイルを更新してリビルドするだけでクライアントも追随する。

使ってみた感想:

  • 良かった点 — API の追加・変更がコンパイルエラーとして検出される。手書きクライアントと違い、フィールド名や型のズレがビルド時に発覚する
  • 気になった点 — 生成されたメソッドのシグネチャが直感と異なることがある。例えば builder パターンではなく positional args で呼び出す API になっていた。手書きなら client.list_stories().status(s).await と書けるところが client.list_stories(project_id, status, limit, offset).await のような形になる
  • 向いている場面 — サーバと CLI を同一リポジトリで管理していて、API の drift を型レベルで防ぎたいとき。OpenAPI の drift check (openapi:check) と組み合わせると、「定義がズレていたらビルドが通らない」という強いガードになる

生成されたクライアントの使用例(シグネチャは progenitor 版のイメージ):

// builder パターンではなく positional args で呼び出す
let stories = client
    .list_stories(project_id, Some(status), Some(limit), None)
    .await?;

次に試す(未実施)

  • progenitor の builder feature が追加されたかどうか確認する。バージョンによっては builder スタイルの生成が選べるかもしれない
a month ago
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
a month ago
Sign in to read
Friend only
This note is available to friends only.
2 months ago
Sign in to read

バーンダウンの理想線は「初回スコープ固定」ではなく日次スコープのスナップショットで引く

イテレーションのバーンダウンチャートを実装するとき、最初に「コミットされたスコープポイント」を1つ凍結して理想線を引くのが素直な発想だが、途中でスコープが増減すると理想線と実線がズレて意味を失う。今回は、スコープ自体も日次スナップショットに含めて、各日の理想線をその日のスコープを基準に引くように変えた。

最初の実装はこんな構造だった。

  • イテレーションテーブルに burndownScopePoints(初回コミットスコープ)を持たせる
  • 1日1回スナップショットを取り、remaining_points だけを記録する
  • 理想線は burndownScopePoints を始点として直線で引く

これだと、途中で「やっぱりこのストーリーも入れる/落とす」が起きたときに、理想線は古いスコープのまま固定されて、現実とまったく合わなくなる。新しい構造はこう。

  • スナップショットテーブルに scope_points カラムを追加(migration 0041)
  • 毎日のスナップショット時に remaining_points と scope_points を両方記録する
  • 当日のスコープが残っていなければ live 集計(Rejected を除いた合計)でフォールバック
  • イテレーション側の burndownScopePoints は捨てる
// Before: 初回スコープを凍結
await freezeScopeIfUnset(db, iterationId, committedScope);
await db.insert(snapshots).values({
  iterationId,
  snapshotDate: today,
  remainingPoints: remaining,
});

// After: 日次でスコープも記録
await db.insert(snapshots).values({
  iterationId,
  snapshotDate: today,
  scopePoints: scope,        // ← 当日のスコープも保存
  remainingPoints: remaining,
});

ポイントは、「変動しうる前提値」をスナップショットの外に置かないこと。理想線・実線・スコープ線の3本を同じ時系列ベースで描けるようになるので、後からバーンアップにも自然に拡張できる。

今回やったこと(実施済み)

  • マイグレーションでスナップショットに scope_points を追加
  • 日次バッチ・API両方で scope_points を保存
  • 理想線は「各日のスコープ」を anchor に再計算
  • バーンアップタブを追加するときに、同じ scope_points 列をそのまま使えた
2 months ago
Friend only
This note is available to friends only.
2 months ago
Sign in to read
Friend only
This note is available to friends only.
2 months ago
Sign in to read
Friend only
This note is available to friends only.
2 months ago
Sign in to read
Friend only
This note is available to friends only.
2 months ago
Sign in to read
Friend only
This note is available to friends only.
2 months ago
Sign in to read
Next Page