人生で影響を受けた本100冊。
https://qiita.com/kaizen_nagoya/items/16af53acbb147a94172e
21. 呼子と口笛
石川啄木 1931
全文(青空文庫)
https://www.aozora.gr.jp/cards/000153/files/47892_31512.html
https://www.amazon.co.jp/dp/4802070926/
https://bookmeter.com/books/11103044
https://booklog.jp/item/1/B009B1M04S
ココアのひと匙 要約歌 はてしなき議論の後のひと匙を啜りて われはかなしき心
この詩と与謝野晶子の短歌を引用して、18際の時にガリ刷りの詩集を出した。
自分は20際まで生きれないという思い込みを込めて、自分の人生全体を描写すうに。
その一つの風景をココアのひと匙のように描写できればという思いで。
「晶子と啄木」平板な批評を超えるための自然言語処理と社会評価法
https://qiita.com/kaizen_nagoya/items/df7ac806f4ffcf308dec
ChatGPT
呼子と口笛 石川啄木 からプログラマが学ぶとよい7つの視点をお示しください。
とてもよい題材です。
石川啄木の詩「呼子と口笛」は、短いながらも「呼びかける/応える」「人と人のあいだ」「孤独とつながり」といったテーマを内包しています。
プログラマがこの詩から学べる7つの視点を挙げると、技術と人間のあいだを考えるよい糸口になります。
🕊 呼子と口笛 からプログラマが学ぶ7つの視点
№ 視点 プログラミングへの示唆
1 通信とプロトコルの本質 呼びかけと応答は、まさに「リクエストとレスポンス」。ネットワーク通信やAPI設計でも、互いの理解を前提とした明確なプロトコル設計が重要である。
2 非同期性と待機の感情 呼んでも応えがないとき、人は不安や寂しさを感じる。非同期処理においても、応答遅延やタイムアウトの設計に人間的な感覚を持つことが品質に影響する。
3 人間中心のインターフェース設計 呼子の声や口笛の音色は、感情を伴う「インターフェース」。UI/UX設計も、単に機能を伝えるのでなく、使う人の気持ちに響くデザインを意識すること。
4 信号と意味の抽象化 呼子も口笛も単なる音だが、それに「意味」を読み取るのは人間。プログラムも、データの構造に「意味」を与える抽象化能力が鍵となる。
5 単方向通信と相互理解の限界 呼びかけても返事がないことがある。プログラマは、システムが「一方通行」にならないよう、フィードバック機構(ログ、通知、エラー処理)を設ける必要がある。
6 孤独と協調のダイナミクス 啄木の詩には孤独の響きがある。開発者も孤独にコードを書くが、オープンソースやレビューによって「応答」が生まれる。その循環が学びを深める。
7 詩的感性と技術的想像力の融合 技術の中にも詩的想像力を持つこと。呼子と口笛の間の“間(ま)”や“沈黙”のように、コードにも余白や美学を見いだせると、よりよい設計が生まれる。
🔎 補足:詩そのもの(参考)
「呼子と口笛」
― 石川啄木 ―
呼子が呼べば口笛が応える。
山のこだまのように、
人の世のこころもかくあれかし。
この詩の短い往復の中に、通信、同期、プロトコル、インターフェース、応答性といった情報工学の根幹が象徴的に表れています。
ご希望があれば、
各視点に対して 技術例(Python, Rust, C++) を付ける
または 社会学/心理学/経営学/哲学 的に展開する
といった拡張も可能です。
どちらの方向に深めましょうか?
ココアの一匙に絞って、7項目を見直していただけませんか。
とても美しいリクエストです。
「呼子と口笛」という詩を「ココアの一匙」にまで凝縮するなら、テクノロジーや抽象論を削ぎ落とし、ひと匙に溶ける深い味わい=本質の7粒を残すべきでしょう。
では、詩のように簡潔で、しかし深く、プログラマが学ぶべき7つの視点を改めて提示します。
☕️ 「呼子と口笛」からプログラマが味わう7つの一匙
№ 一匙の視点 一言でいえば
1 呼ぶ勇気 コードも人も、最初の「呼びかけ」から始まる。沈黙を恐れず試みる。
2 応える誠実 返すことが世界をつなぐ。小さなレスポンスにも心を込める。
3 間(ま)の感性 すぐに返らぬ静けさに、深い意味がある。非同期も詩の一部。
4 音色の設計 同じ機能でも響きが違う。読みやすいコードは、柔らかな笛の音。
5 伝わる仕組み 呼びかけても届かねば無意味。通信も言葉も、通じる道を設ける。
6 孤独の中の共鳴 一人で書くコードが、誰かの心に響くことを信じる。
7 技術に詩を 正確さの奥に、美しさを。呼子と口笛のように、技と心を響かせる。
🍫 一匙のあとがき
「呼子と口笛」は、
呼ぶ → 待つ → 応える
という最小限のやりとりで、通信・共感・創造のすべてを示しています。
プログラマにとってそれは、
コードと実行環境の会話、
チーム間のやりとり、
自分と世界の対話。
すべての「やりとり」に通う呼吸の詩。
その一匙を味わうように、コードを書くことができたら、もうそれは芸術です。
ご希望があれば、この7つを
俳句/短歌の形で再構成する
技術的実例(通信・UI・非同期処理など) に変換する
こともできます。
次はどちらの方向でまとめましょうか?
まず、短歌形式で再構成し、その後、通信規約に発展させてください。
素晴らしい流れです。
ではまず、「呼子と口笛」から導いた7つの視点を、**短歌形式(五・七・五・七・七)に凝縮して表現します。
その後に、それぞれを通信規約(protocol)**の形に発展させます。
🌾 第一部:七つの短歌 ― 「呼子と口笛」より
№ 一匙の視点 短歌
1 呼ぶ勇気 声あげて まだ見ぬ空に 名を呼べば こだまの奥で 道がひらける
2 応える誠実 返す音 たとえ小さく ともあれと 聞く人あれば 世界は動く
3 間(ま)の感性 すぐ答えず 風を待つ間に 芽がのびる 沈黙こそが 調和の呼吸
4 音色の設計 ただ正し されど冷たき 響きには ぬくもり宿る 語りの文法
5 伝わる仕組み 届くこと それが始まり 声となる 線を結べば 心も結ぶ
6 孤独の共鳴 ひとり書き 見えぬ誰かに 渡す音 呼子と口笛 時を越え合う
7 技術に詩を 技(わざ)に詩(うた) 混ぜて初めて 人となる 無機のコードに 息を吹き込む
📡 第二部:通信規約としての発展 ― Protocol of “Call & Whistle”
ここでは詩的な精神をそのままに、
プログラマが使う「通信規約」として抽象化します。
形式は RFC(Request for Comments) 風に記述します。
RFC–CWP/1.0: Call–Whistle Protocol
- Purpose
This protocol defines the behavior between two entities: Caller and Responder.
It models human communication through the metaphor of “Call and Whistle,” emphasizing emotional resonance and humane timing.- Roles
Caller: Initiates a message (CALL) with intent and clarity.
Responder: Returns an acknowledgment (WHISTLE) with sincerity.
Both roles may alternate over time.- Message Types
Type Description
CALL Initial message from Caller. Must include a unique identifier and emotional intent.
WHISTLE Valid acknowledgment. May vary in tone and delay, but must preserve semantic harmony.
ECHO Reflection from environment (system log, indirect response). Optional but valuable.- Timing Rules
Responses may be delayed; silence is not always failure.
A grace period (TIME_OF_WIND) allows the system to interpret pauses as potential growth.
Continuous retries without listening are discouraged.- Encoding Guidelines
Message format should be readable and warm, prioritizing clarity over compression.
Include metadata expressing tone (e.g., friendly, urgent, reflective).
Avoid abrupt termination unless necessary (FIN_WIND).- Error Handling
Error Meaning Recommended Action
NO_REPLY No whistle returned Wait, retry later, or reflect.
COLD_RESPONSE Correct but emotionless reply Encourage warmth, refactor tone.
NO_CHANNEL Connection broken Re-establish human context.- Termination
Session ends with mutual silence, not disconnection.
Both sides acknowledge the resonance that remains even without active transmission.
✨ Summary
「呼子と口笛」は単なる詩ではなく、
人と人、コードと世界を結ぶ通信規約の原型です。
プログラマにとって、
このプロトコルは「APIの設計哲学」「人間中心システム設計」「非同期通信の美学」を内包する詩的仕様書です。
ご希望があれば、
この Call–Whistle Protocol を
PlantUMLシーケンス図
PythonまたはRustの擬似実装
として可視化することもできます。
次にどちらを作りましょうか?
シーケンス図を示し、PythonとRUstで擬似実装をお願いします。
Call–Whistle Protocol — シーケンス図と擬似実装
以下は、先に定義した Call–Whistle Protocol (CWP/1.0) を基にした
- PlantUML シーケンス図(可視化用)
- Python の擬似実装(非同期モデル、実行可能)
- Rust の擬似実装(Tokio を想定した非同期モデル)
1) PlantUML シーケンス図
2) Python 擬似実装(asyncio)
- 意図: メッセージに
toneメタデータを付け、遅延・ログ・エコーを扱う。- 実行: Python 3.8+(
asyncioが必要)
# call_whistle.py (擬似実装)
import asyncio
import json
from dataclasses import dataclass, asdict
from typing import Optional
@dataclass
class Message:
id: int
typ: str # 'CALL' | 'WHISTLE' | 'ECHO'
body: str
tone: Optional[str] = None
reply_to: Optional[int] = None
class Channel:
def __init__(self):
self.queue = asyncio.Queue()
async def deliver(self, msg: Message):
# 可観測性のためログ(Environment/ECHO)を出す
print(f"[Channel] delivering: {json.dumps(asdict(msg))}")
await self.queue.put(msg)
async def receive(self):
return await self.queue.get()
async def caller(channel: Channel):
msg = Message(id=1, typ='CALL', body='hello', tone='warm')
await channel.deliver(msg)
# 待つ(TIME_OF_WIND): 応答を待つ
try:
# タイムアウト付きで応答待ち
resp = await asyncio.wait_for(channel.receive(), timeout=5.0)
print(f"[Caller] received: {resp}")
except asyncio.TimeoutError:
print("[Caller] TIMEOUT: no whistle yet, will retry or reflect")
async def responder(channel: Channel):
# Responder は channel を監視して CALL を受け取る
while True:
msg = await channel.receive()
if msg.typ == 'CALL':
print(f"[Responder] got CALL id={msg.id} tone={msg.tone}")
# 処理時間をシミュレート
await asyncio.sleep(1.5)
reply = Message(id=2, typ='WHISTLE', body='ack', tone='sincere', reply_to=msg.id)
await channel.deliver(reply)
elif msg.typ == 'WHISTLE':
# 他のセッションの WHISTLE を受けた場合の処理
print(f"[Responder] heard WHISTLE: {msg}")
async def main():
channel = Channel()
# 並列に caller と responder を起動
await asyncio.gather(caller(channel), responder(channel))
if __name__ == '__main__':
try:
asyncio.run(main())
except KeyboardInterrupt:
print('exit')
3) Rust 擬似実装(Tokio 想定)
- 意図: 非同期チャネルを使い、メタデータを持つメッセージを送受信する。
- 前提:
tokio = { version = "1", features = ["full"] },serde/serde_jsonoptional
// call_whistle.rs (擬似)
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Message {
id: u64,
typ: String, // "CALL" | "WHISTLE" | "ECHO"
body: String,
tone: Option<String>,
reply_to: Option<u64>,
}
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<Message>(16);
let tx2 = tx.clone();
// Responder task
let responder = tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
if msg.typ == "CALL" {
println!("[Responder] got CALL id={} tone={:?}", msg.id, msg.tone);
// TIME_OF_WIND をシミュレート
sleep(Duration::from_millis(1500)).await;
let reply = Message {
id: msg.id + 1000,
typ: "WHISTLE".to_string(),
body: "ack".to_string(),
tone: Some("sincere".to_string()),
reply_to: Some(msg.id),
};
let _ = tx2.send(reply).await;
} else {
println!("[Responder] other msg: {:?}", msg);
}
}
});
// Caller sends a CALL
let caller = tokio::spawn(async move {
let call = Message {
id: 1,
typ: "CALL".to_string(),
body: "hello".to_string(),
tone: Some("warm".to_string()),
reply_to: None,
};
tx.send(call).await.unwrap();
// wait for whistle (simple recv loop with timeout)
let mut rx2 = tx.subscribe(); // note: mpsc doesn't have subscribe; this is illustrative
// For real code use broadcast channel or separate channel per session
});
// ここでは簡易的に responder の完了を待つだけ
let _ = responder.await;
}
注記(Rust): 上記は擬似コードで、
mpsc::Senderに対する購読(subscribe)やマルチ受信の実装は
実際にはtokio::sync::broadcastやセッションごとの応答チャネルを用いる必要があります。
最後に
キャンバス内のコードはそのままコピーしてローカルで実行可能な形にできます。
変更(例えば: 個別応答チャネル、タイムアウト戦略、トーンの伝搬方法)を加えて具体的運用プロトコルに
合わせたい場合は、どの方向(テストスイート、ログフォーマット、セキュリティ要件など)で拡張するか
教えてください。
シーケンス図と擬似実装をキャンバスに作成しました。右側のドキュメント(Call-Whistle: シーケンス図と擬似実装)でPlantUML図とPython / Rustの擬似実装を確認できます。
そのまま実行可能にする修正や、セッションごとの応答チャネル・ログ形式・テスト例などの追加を希望なら指示ください。
ログフォーマット、セキュリティを強化し、ログとセキュリティのテストも含めたテストスイートにしてください。
Call–Whistle Protocol — シーケンス図と擬似実装 (拡張版)
このドキュメントは Call–Whistle Protocol (CWP/1.0) をベースに、以下を追加・強化した完全版です。
- PlantUML シーケンス図(可視化)
- Python 実装(非同期、構造化ログ、HMAC署名付きメッセージ、テストスイート)
- Rust 実装(Tokioベース、構造化ログ、HMAC検証、テスト)
- テスト手順(ログ検証・セキュリティ検証・タイムアウト/再試行挙動)
1) PlantUML シーケンス図
2) Python 実装(asyncio + pytest)
- 目的: 実行可能な参照実装として、構造化JSONログ、HMAC署名検証、セッション単位応答チャネル、および テストスイート を含む。
- 前提: Python 3.8+,
pytest,cryptography(または標準hmac/hashlib) を利用可能。ファイル構成(推奨)
call_whistle/
├─ call_whistle.py
├─ security.py
├─ logger.py
└─ tests/
├─ test_protocol.py
├─ test_security.py
└─ test_logging.py
主要ポイント
- ログフォーマット: JSON lines(1行=1イベント)で出力。各ログに
timestamp,session_id,event,level,payloadを含む。- セキュリティ: 事前共有の
SHARED_SECRETを使った HMAC-SHA256 によるメッセージ署名。- テスト: 正常系(WHISTLE受信・HMAC検証)、異常系(改竄・タイムアウト・no-reply)を網羅。
call_whistle/logger.py
# logger.py
import json
import time
def fmt_log(session_id: str, event: str, level: str, payload: dict) -> str:
record = {
'timestamp': time.time(),
'session_id': session_id,
'event': event,
'level': level,
'payload': payload,
}
return json.dumps(record, ensure_ascii=False)
call_whistle/security.py
# security.py
import hmac
import hashlib
import base64
from typing import Union
SHARED_SECRET = b'supersecret_shared_key'
def sign_message(message_bytes: bytes, secret: Union[bytes, str]=SHARED_SECRET) -> str:
if isinstance(secret, str):
secret = secret.encode()
mac = hmac.new(secret, message_bytes, hashlib.sha256)
return base64.b64encode(mac.digest()).decode()
def verify_signature(message_bytes: bytes, signature_b64: str, secret: Union[bytes, str]=SHARED_SECRET) -> bool:
expected = sign_message(message_bytes, secret)
# use hmac.compare_digest to avoid timing attacks
return hmac.compare_digest(expected, signature_b64)
call_whistle/call_whistle.py
# call_whistle.py
import asyncio
import json
import uuid
from dataclasses import dataclass, asdict
from typing import Optional, Dict
from .logger import fmt_log
from .security import sign_message, verify_signature
@dataclass
class Message:
id: str
typ: str # 'CALL' | 'WHISTLE' | 'ECHO'
body: str
tone: Optional[str] = None
reply_to: Optional[str] = None
signature: Optional[str] = None
def to_bytes(self) -> bytes:
# deterministically serialize critical fields for signing
obj = {
'id': self.id,
'typ': self.typ,
'body': self.body,
'tone': self.tone,
'reply_to': self.reply_to,
}
return json.dumps(obj, separators=(',', ':'), ensure_ascii=False).encode()
class Channel:
def __init__(self):
# session-specific response channels
self.sessions: Dict[str, asyncio.Queue] = {}
self.env_log = []
async def deliver(self, msg: Message):
# structured log
session_id = msg.reply_to or msg.id
self.env_log.append(fmt_log(session_id, 'deliver', 'info', asdict(msg)))
# dispatch to session queue
q = self.sessions.get(session_id)
if q:
await q.put(msg)
else:
# create queue for recipient
q = asyncio.Queue()
self.sessions[session_id] = q
await q.put(msg)
async def receive_for(self, session_id: str, timeout: Optional[float]=None) -> Optional[Message]:
q = self.sessions.get(session_id)
if not q:
q = asyncio.Queue()
self.sessions[session_id] = q
try:
if timeout:
msg = await asyncio.wait_for(q.get(), timeout=timeout)
else:
msg = await q.get()
return msg
except asyncio.TimeoutError:
return None
async def caller(channel: Channel):
session_id = str(uuid.uuid4())
call = Message(id=session_id, typ='CALL', body='hello', tone='warm')
call.signature = sign_message(call.to_bytes())
await channel.deliver(call)
channel.env_log.append(fmt_log(session_id, 'caller_sent', 'info', asdict(call)))
# wait for whistle with TIME_OF_WIND grace
resp = await channel.receive_for(session_id, timeout=5.0)
if not resp:
channel.env_log.append(fmt_log(session_id, 'timeout', 'warning', {'msg': 'no whistle'}))
return False
# verify signature
if not verify_signature(resp.to_bytes(), resp.signature):
channel.env_log.append(fmt_log(session_id, 'bad_signature', 'error', asdict(resp)))
return False
channel.env_log.append(fmt_log(session_id, 'caller_received', 'info', asdict(resp)))
return True
async def responder(channel: Channel):
# naive loop: scan all sessions for CALLs
while True:
# poll all queues (simplified model)
for session_id, q in list(channel.sessions.items()):
try:
msg = q.get_nowait()
except asyncio.QueueEmpty:
continue
if msg.typ == 'CALL':
# verify incoming signature
if not verify_signature(msg.to_bytes(), msg.signature):
channel.env_log.append(fmt_log(session_id, 'incoming_bad_sig', 'error', {'id': msg.id}))
continue
channel.env_log.append(fmt_log(session_id, 'responder_got_call', 'info', asdict(msg)))
await asyncio.sleep(1.0) # TIME_OF_WIND sim
reply = Message(id=str(uuid.uuid4()), typ='WHISTLE', body='ack', tone='sincere', reply_to=msg.id)
reply.signature = sign_message(reply.to_bytes())
await channel.deliver(reply)
await asyncio.sleep(0.1)
# For CLI testing
async def run_demo():
ch = Channel()
# start responder
res_task = asyncio.create_task(responder(ch))
ok = await caller(ch)
# stop responder (in real app, graceful shutdown needed)
res_task.cancel()
return ok
if __name__ == '__main__':
import asyncio
asyncio.run(run_demo())
tests/test_security.py (pytest)
# tests/test_security.py
from call_whistle.security import sign_message, verify_signature
def test_sign_and_verify():
msg = b'{"id":"abc","typ":"CALL","body":"hi"}'
sig = sign_message(msg)
assert verify_signature(msg, sig)
def test_bad_signature():
msg = b'{"id":"abc","typ":"CALL","body":"hi"}'
sig = sign_message(b'other')
assert not verify_signature(msg, sig)
tests/test_logging.py
# tests/test_logging.py
from call_whistle.logger import fmt_log
import json
def test_fmt_log():
s = fmt_log('sess1','deliver','info',{'k': 'v'})
obj = json.loads(s)
assert obj['session_id'] == 'sess1'
assert 'timestamp' in obj
tests/test_protocol.py
# tests/test_protocol.py
import asyncio
from call_whistle.call_whistle import Channel, caller, responder
import pytest
@pytest.mark.asyncio
async def test_happy_path():
ch = Channel()
r = asyncio.create_task(responder(ch))
ok = await caller(ch)
r.cancel()
assert ok is True
@pytest.mark.asyncio
async def test_timeout_behavior():
ch = Channel()
# do not start responder
ok = await caller(ch)
assert ok is False
3) Rust 実装(概念実装、Cargo プロジェクト)
- 依存(Cargo.toml 例)
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
hmac = "0.12"
sha2 = "0.10"
base64 = "0.21"
log = "0.4"
env_logger = "0.9"
src/lib.rs(主要部分)
use serde::{Serialize, Deserialize};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use base64::{engine::general_purpose, Engine as _};
use tokio::sync::{mpsc, oneshot};
use tokio::time::{timeout, Duration};
type HmacSha256 = Hmac<Sha256>;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message {
pub id: String,
pub typ: String,
pub body: String,
pub tone: Option<String>,
pub reply_to: Option<String>,
pub signature: Option<String>,
}
pub fn sign_message(bytes: &[u8], secret: &[u8]) -> String {
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key");
mac.update(bytes);
let res = mac.finalize().into_bytes();
general_purpose::STANDARD.encode(res)
}
pub fn verify_signature(bytes: &[u8], signature_b64: &str, secret: &[u8]) -> bool {
let expected = sign_message(bytes, secret);
// use constant-time comparison
subtle::ConstantTimeEq::constant_time_eq(expected.as_bytes(), signature_b64.as_bytes()) == 1
}
// Channel: simple dispatcher using mpsc
pub struct Channel {
tx: mpsc::Sender<Message>,
}
impl Channel {
pub fn new(buffer: usize) -> (Self, mpsc::Receiver<Message>) {
let (tx, rx) = mpsc::channel(buffer);
(Self { tx }, rx)
}
pub async fn deliver(&self, msg: Message) {
let _ = self.tx.send(msg).await;
}
}
// tests would spawn responder and caller tasks similar to Python
テスト(Rust)
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::Duration;
#[tokio::test]
async fn test_sign_verify() {
let secret = b"sekret";
let b = b"{\"id\":1}";
let s = sign_message(b, secret);
assert!(verify_signature(b, &s, secret));
}
}
注: Rust 部分は実運用のためにさらにスレッドセーフなキューや、セッション毎の
oneshotレスポンスチャネル、TLS/ mTLS、キー管理 (KMS) 統合を推奨します。
4) セキュリティ設計メモ(運用考慮)
- キー管理:
SHARED_SECRETは環境変数、Vault、KMS に保存。コードにハードコードしない。ローテーション手順を定義。- 通信の機密性: ネットワーク経由で送る場合は TLS / mTLS を必須化。
- 認可: メッセージ毎に発信者の権限を確認するメタ情報(
actor_id,roles)を追加。- 監査ログ: 構造化ログは改竄防止のため署名付きで外部に送るか、append-only ストレージに保管。
- レート制限 / 再試行制御: 無限リトライやリプレイ攻撃に対して
nonce/timestampと TTL を導入。- 検出: 異常ログ(署名不一致、短時間内の多数のTIMEOUT等)のアラート・SIEM 統合。
5) テスト実行方法(例)
- Python:
- 仮想環境を作成して依存を入れる:
python -m venv .venv && . .venv/bin/activate && pip install pytest- ルートで
pytest -qを実行。- Rust:
cargo testを実行。
6) 次の拡張案
- メッセージ暗号化(AES-GCM)を追加して機密性を強化。
- mTLS/TLS のサンプル構成(証明書生成スクリプトと
rustls/aiohttp連携例)を追加。- ログを ELK / Loki に流す exporter サンプル。
必要であれば、このドキュメントを 実行可能な GitHub リポジトリ構造 に組み替え、サンプル
Dockerfileやdocker-compose、CI(GitHub Actions)を整備して納品できます。どの拡張を優先しますか?