第1幕: 現場 ── 書いてあるのに、無い
公式のドキュメントを渡して AI に書かせたのに、ビルドが30か所で落ちた。予約の受け付けは、明日の朝10時に始まる。
わたしは、ブドウ畑の端の農道に停めた自分の軽自動車の運転席で、ゆうべ23時41分に届いたメールを、もう一度開いていた。CI からの失敗の通知だ。CI は、プッシュするたびにビルドやテストを自動で走らせて、通ったか落ちたかを知らせてくれる仕組みのこと。件名の横に、赤いばつ印が付いている。
本文のリンクからログを開いた。ビルドの段のところに、エラーの行が縦に並んでいる。一行ごとに、ファイルの名前と位置、それからエラーの中身が書いてある。ゆうべ、布団の中で数えたときは、九つのファイルで三十個だった。
わたしは、エラーの行を指でなぞりながら、もう一度数えた。TS2305 という番号の付いた行が、何度も出てくる。番号の横に出ている名前は、同じものが繰り返されている。createShipment。quoteRates。また createShipment。三十まで数えて、指を止めた。数え直しても、三十だった。
窓の外で、朝露に濡れたブドウの葉が光っていた。ひなたワイナリーの畑は、ゆるい丘の斜面に、畝が等高線みたいに並んでいる。房は、もう黒に近い紫に色づいていた。収穫が近いのだと、素人のわたしにも分かった。
フロントガラスの向こう、畝の入り口のところで、葛城さんが誰かに紙を一枚渡しているのが見えた。葛城さんは、このワイナリーの二代目で、わたしの会社のお客さんだ。長靴に、つばの広い帽子。窓を少し下げると、声が半分だけ聞こえた。苗木を頼むのに、新しく刷ったの。区画はこれで。
葛城さんの前に立っていたのは、わたしより、たぶん年下の女の人だった。短い髪が、寝癖みたいにあちこち跳ねている。薄手の上着の左の袖から先だけが、茶色い革の大きな手袋に包まれていた。胸元で、紐に下げた小さな笛が揺れている。
女の人は、紙に目を落とさなかった。
一言、礼を言ったように見えた。それから紙を二つに、もう二つに畳んで、上着のポケットに入れた。開きもしなかった。
畝のあいだに張られた防鳥ネットの、いちばん高い支柱の上に、鳥がとまっていた。こげた木みたいな色の体に、栗色の肩。尾の先にだけ、白い帯が入っている。鷹だ、と思った。本物を見るのは初めてだった。鷹は、くちばしで胸の羽を整えていた。
女の人は、わたしの車の前を横切って、畝のあいだへ入っていった。急がずに、房を見ながら歩いていく。ときどき、房の下に指を入れて、持ち上げるようにしてから、また離した。
わたしは、ノートパソコンを閉じて車を降りた。約束は9時で、まだ8時半を少し過ぎたところだった。
「葛城さん、おはようございます。すみません、早く着いてしまって」
「おはよう、日比野さん。いいのいいの。早いほうが助かる」
葛城さんは、帽子のつばを指で上げた。わたしは、ゆうべのことを言おうとして、言えなかった。明日の朝10時に、このワイナリーの収穫体験の予約を受け付け始める。その画面を作ったのは、うちの会社で、担当はわたしだ。
「今日はね、鷹匠さんに来てもらってるの。ムクドリが毎朝来るのよ。摘むのは明後日からだから、それまで持たせたくて」
葛城さんが、畝のほうへ手を振った。女の人が振り返って、こちらへ戻ってきた。
「鷹宮さん。こちら、うちのサイトを作ってくれてる日比野さん」
「鷹宮です」
女の人は、それだけ言って、軽く頭を下げた。わたしも頭を下げた。
「葛城さん。地図、ありがとうございます。先に、歩いてきます」
鷹宮さんは、また畝のほうへ歩いていった。葛城さんは、蔵の様子を見てくると言って、坂の上の建物へ戻っていった。
わたしは、車のボンネットにノートパソコンを置いた。金属がもう、朝日でほんのり温かい。
ゆうべのことを、順番に思い出した。
わたしは、地元の小さな Web 制作会社で働いている。社員は四人で、わたしは入って二年目だ。ひなたワイナリーのサイトは、この春に先輩から引き継いだ。今年の秋の新しい企画で、収穫体験の予約をサイトで受け付けることになった。それに合わせて、直売のワインをクール便で送れるようにしてほしい、と葛城さんに頼まれていた。
発送のラベルを作る処理は、去年、先輩が組んだものだ。配送業者のはこび便が出している SDK を使っている。SDK は、その会社のサービスをプログラムから使うための部品で、名前は hakobi-sdk という。
昨日の夕方、わたしは AI に、クール便の発送の画面と処理を作ってもらった。会社の四人とも、コードの半分くらいは AI のコーディングエージェントに書いてもらっている。頼みごとを文で伝えると、自分でリポジトリのファイルを開いて、書いて、動かして、結果を返してくる道具だ。
頼むときには、はこび便の公式サイトのリファレンスのページを、丸ごと貼った。
そうするようになったのは、先月からだ。別の改修で、AI が SDK の古い書き方を使ってきたことがあった。AI は、作られたときに読んだ大量の文章をもとに書く。だから、読んだときより新しい書き方を知らないことがある。それで手戻りになってから、わたしは、AI の覚えていることより、公式のページのほうが新しいはずだと思って、頼むたびにページを貼るようにしていた。
AI は、画面と処理を作って、できましたと報告した。わたしは画面を開いて、見た目を確かめて、夜にプッシュした。23時41分に、通知が来た。
ボンネットの上で、ログを開き直して、一行に目を留めた。
| |
行の頭の src/shipping/cool-label.ts(3,10) は、エラーのあったファイルと、その3行目の10文字目、という意味だ。
このサイトは TypeScript で書いている。TypeScript は、JavaScript に型を付けた言語だ。公開する前に、tsc というコンパイラで、型が合っているかを調べてから JavaScript に変換する。この工程を、うちではビルドと呼んでいる。Node.js(以下 Node)は、Node 24 なら .ts をそのまま動かせる。ただ、そのときは型を取り除いて動かすだけで、型は調べない。だから、手元で画面が動いても、ビルドで落ちることがある。
TS2305 は、そのモジュールに、その名前の export が無い、というエラーだ。ライブラリは、外から使ってほしい関数や値に名前を付けて、外に出している。これを export と言う。使う側は、import { createShipment } from 'hakobi-sdk' のように、名前で取り込む。エラーは、hakobi-sdk は createShipment という名前を外に出していない、と言っていた。
わたしは、ブラウザで、はこび便の公式サイトを開き直した。昨日貼ったのと同じ、リファレンスのページだ。
createShipment は、ちゃんとあった。
荷物を登録して、送り状を作ります。見出しの下に、そう書いてある。quoteRates も、cancelShipment もあった。
書いてある名前を、無い、と言われている。
わたしは、ボンネットに手をついて、画面とログを交互に見た。どちらも、本物の画面だった。
顔を上げると、畝のずっと奥で、鷹宮さんがしゃがんでいた。何かを指でつまんで、目の前に持ってきて見ている。ここからは、何をつまんでいるのか見えなかった。上着のポケットから、葛城さんの紙の角が、少しだけのぞいていた。
第2幕: 再現 ── 来年の図
9時少し前に、葛城さんが坂を下りてきた。
「日比野さん、鷹、見ていく? せっかくだから」
断る理由が思いつかなかった。わたしはノートパソコンを閉じて、葛城さんのあとについて畝のあいだへ入った。土が柔らかくて、スニーカーの底に湿った重さがつく。房の近くを通ると、甘い匂いがした。
鷹宮さんは、拳の上に鷹を据えて、畝の端に立っていた。鷹は、いつの間にか支柱から下りてきていた。
葛城さんが、ポケットを指さした。
「鷹宮さん、さっきの地図、見てくれた? 鳥が来るのは、たぶん B のほうだと思う。赤いのは、あっちだから」
鷹宮さんは、葛城さんのほうを見て、小さく頷いた。そして、B のほうへは行かなかった。
鷹宮さんは、畝を二本またいで、反対側へ歩いていった。斜面の下、畑の境に植えられた木の列の端まで行って、そこで止まった。風が、木の列のほうから吹いてくる。鷹宮さんは、風を背にするのではなく、風を受ける向きに立っていた。
拳が、少しだけ上がった。
鷹が、拳を蹴った。低く、木の列に沿って飛んでいく。羽の音は、思ったより静かだった。
次の瞬間、木の列が鳴った。葉の陰から、小さな鳥が一斉に飛び出した。何十羽、もっといたかもしれない。群れは、黒い煙みたいに一つにまとまって、畑の上で一度大きく曲がって、丘の向こうへ流れていった。
鷹は、群れを追わずに戻ってきて、防鳥ネットの支柱にとまった。
「あら」
葛城さんが、首をかしげた。
「C のほうだったの。でも、C は白のはずなのに。シャルドネで……」
そこまで言って、葛城さんは自分で吹き出した。
「やだ、それ来年の図だ。C のメルローを抜いて、シャルドネに植え替えるの、収穫のあとなのよ。苗木を頼むのに刷ったから、植え替えたあとの区画で描いてあるの」
わたしは、横でそれを聞いていた。
葛城さんは、蔵から電話だと呼ばれて、坂を上がっていった。畝の端に、鷹宮さんとわたしが残った。
「あの」
わたしは、自分から声をかけていた。
「すみません、さっきの地図……葛城さんが、わざわざ刷ってきたものですよね。開かなかったのは、どうしてですか」
鷹宮さんは、ポケットの上から紙を軽く押さえた。
「来年の、だから」
「でも、来年のでも、今年と同じところのほうが多いんじゃないですか。見れば、早かったのかなって」
口に出したとたん、失礼だったかもしれない、と首のあたりが熱くなった。鷹宮さんは、気にした様子もなく、近くの房を一つ、指の背で持ち上げた。黒に近い紫。粒が詰まって、重そうだった。
「今日の畑、見たいので」
「今日の……」
「どこが同じで、どこが違うか。歩くまで、分かんないです」
それだけ言うと、鷹宮さんは、房を戻して、拳の鷹を見た。
わたしは、ボンネットのパソコンに戻りながら、鷹宮さんの言ったことを考えていた。
葛城さんの刷った図は、葛城さんの手元でいちばん新しい紙だった。でも、描いてあったのは、来年の畑だった。今年の C には、まだメルローが実っている。
いちばん新しい紙が、今日の畑とは限らない。
わたしが貼ったページは、はこび便の公式サイトの、いま出ているページだった。いちばん新しいはずの紙だ。でも、それは、何の紙だったんだろう。
パソコンを開いて、サイトのリポジトリの package.json を開いた。依存しているパッケージと、その版(バージョン)が書いてあるファイルだ。
| |
^5.3.0 は、5.3.0 以上で、6 にはならない範囲なら、新しいものを入れてよい、という指定だ。版の番号は、たいてい三つの数字でできていて、先頭の数字をメジャーバージョンと呼ぶ。先頭の数字が変わるときは、名前が消えたり、変わったりすることがある。だから、先頭が 5 のまま、と決めてある。
実際に入っている版は、package-lock.json に書いてある。lockfile と呼ばれるファイルで、npm install したときに入れた版を、一つずつ書き留めておくものだ。これがあれば、誰がいつ入れ直しても、同じ版が入る。開くと、hakobi-sdk は 5.3.0 だった。リポジトリの node_modules というフォルダに入っている実物も、同じ 5.3.0 だ。node_modules は、入れた依存のパッケージが、そのまま置かれている場所のことだ。
もう一度、はこび便の公式サイトのリファレンスを開いた。ページの右上に、小さな選択欄があった。今まで、気にしたこともなかった。表示は v6.0.0。サイトのお知らせを見ると、v6 は先月出ていた。
選択欄を v5.3.0 に切り替えた。
ページの中身が変わった。createShipment が消えて、そこに createLabel が出てきた。quoteRates は getRates になった。cancelShipment は cancelLabel。schedulePickup は、どこにも無かった。
6 と、5。
わたしは、ログに戻って、エラーをもう一度数えた。今度は、種類で分けた。名前が無い、という TS2305 が二十四。残りの六つは、関数に渡しているものの形が違う、返ってくるものの形が違う、というエラーだった。存在しない四つの名前が、九つのファイルの中で、何度も使われていた。
AI は、わたしが貼ったページのとおりに書いた。
AI は、自分の覚えていることよりも、会話に入れて渡されたもの(これをコンテキストと呼ぶ)に沿って書くことが多い。必ずではないけれど、多い。だから先月から、わたしはページを貼るようにした。昨日は、それが裏目に出た。
確かめたくなった。昨日の頼み方で、AI は、ページから何を受け取っていたのか。
わたしは、貼ったページから、版と名前を抜き出す関数を書いた。AI に渡す API の資料を、こういう形で表すことにした。
| |
pkg はパッケージの名前、version は版、names は使える名前の一覧だ。
| |
リファレンスのページは、先頭の見出しに # hakobi-sdk v6.0.0 リファレンス のように版が入っていて、関数ごとに ### createShipment(options) のような見出しが付いている。正規表現で、その二つを拾う。\S+ は空白以外の文字の続き、[^\s(]+ は空白と ( 以外の文字の続きだ。末尾の m は行ごとに ^(行の頭)を効かせる指定、g は一致するものを全部探す指定。matchAll は、一致したものを全部取り出す。[...] で、それを配列に広げている。m[1] や head[1] は、括弧で囲んだ部分に一致した文字列だ。
昨日貼ったページを渡すと、版は 6.0.0、名前は七つ出てきた。COOL_TYPES、cancelShipment、createShipment、quoteRates、schedulePickup、trackShipment、validateAddress。昨日の AI は、この七つを、使える名前として受け取っていたことになる。
この資料が、入っている版と合っているかを確かめるテストを書いた。いまの作りでは赤になるはずのものを先に書いて、本当に赤になるのを確かめておく。最初から緑になるテストだと、今朝のずれを本当に捕まえられるのか分からないからだ。入っている 5.3.0 の名前は、v5 のページと node_modules の中身を見て、テストに手で書いた一覧(installed)にした。
| |
notes は、貼ったページから作った資料だ。assert.ok は、中身が真でなければ失敗にする。二つ目の引数は、失敗したときに出す文だ。一本目は、資料に無い名前が混じっていないか(今朝の事故そのもの)を見る。二本目は、使える名前が資料から漏れていないか(書き忘れ)を見る。片方だけだと、もう片方のずれを見逃す。node --test にかけた。
| |
三本とも赤だった。assert は最初に失敗したところでそのテストを打ち切るので、出てくるのは最初の一件だけだ。一本目は cancelShipment は入っている版に無い で止まり、二本目は cancelLabel が資料に無い で止まった。三本目は、6.0.0 と 5.3.0 が違う、と言っていた。
貼ったページは、公式で、いちばん新しかった。でも、入っている版と、一度も比べていなかった。
わたしは、パソコンの画面を見たまま、直し方を思いついた。ページが v6 なら、入っている SDK を v6 に上げればいい。そうすれば、ページと合う。AI が書いたものも、そのまま——
「日比野さーん。コンテナ、数えるの手伝ってくれる?」
坂の上から、葛城さんの声がした。わたしは、思いついたことを口に出す前に、パソコンを閉じた。
第3幕: 手順を整える ── 実物から作る
10時。わたしは、C 区画の畝の端で、摘み取りに使うプラスチックのコンテナを数えていた。明後日の朝、これにブドウを摘んで入れる。黄色いコンテナが、畝の端に何列も積んである。三十二、三十三、と数えていると、気持ちが少し落ち着いた。
鷹宮さんが、畝の奥から戻ってきた。拳には何も載っていない。鷹は、支柱の上だった。
わたしは、数えかけの列から顔を上げた。
「鷹宮さん。さっきの、すみません、もう一つ聞いてもいいですか。地図を開かないで、どうして鳥のいるところが分かったんですか」
鷹宮さんは、少し考えるように、畝の先を見た。
「図は、描いた日の畑しか、知らないので」
「描いた日の」
「葛城さんが描いた日。あれは、来年の」
それから、鷹宮さんは、自分の歩いてきた順番を、短く言った。下から、風の来るほうから歩いた。色の付いた房を見た。つつかれた粒が、C に多かった。木の列の葉の陰に、鳥がたまっていた。だから、そこから放した。
言い終わると、鷹宮さんは口を閉じた。説明は、それで全部らしかった。
わたしは、コンテナの縁に手を置いたまま、それを自分の仕事に置き換えていった。
図には、描いた日の畑しか載っていない。今日の畑と合っているかどうかは、図のどこにも書いていない。リファレンスのページも同じだ。わたしのサイトに入っている版と合っているかどうかは、ページの側からは、分からない。
それなら。
「あの、さっき思いついたんですけど。入っている SDK を、v6 に上げればいいんですよね。ページは公式で、いちばん新しい。上げれば、ページと合うから、AI が書いたものは、そのまま通る」
しゃべっているうちに、これで決まりのような気がしてきた。ページと、入っている版を合わせる。合わなかったのが原因なのだから、合わせればいい。
返事は、言葉では来なかった。
かわりに、鷹宮さんはしゃがんで、すぐそばの房を一つ、根元のところから指で持ち上げた。房を少し回して、わたしの目の高さに向ける。粒がぎっしり詰まって、手のひらからこぼれそうなくらい重そうだった。黒に近い紫の皮に、白い粉がうっすらと吹いている。明後日には摘まれる房だ。
「今年の房」
鷹宮さんは、それだけ言った。
ちょうどそのとき、葛城さんが、摘み取りの段取りを書いたボードを抱えて、畝の端を通りかかった。
「明後日の朝から、C を摘むからね。7時集合。日比野さん、予約のほうはお願いね」
「は、はい」
「植え替えは、摘み終わってから。今の木を抜くのは、そのあと」
葛城さんは、わたしたちの話は聞いていなかった。段取りを伝えて、そのまま次の区画へ歩いていった。
摘み終わってから、抜く。
わたしは、その言葉に引っかかった。ポケットからスマホを出して、はこび便の公式サイトを開いた。v6 のお知らせの下に、移行ガイドへのリンクがあった。v5 から v6 へ上げるときに、何が変わるかをまとめたページだ。昨日は、開いてもいなかった。
変更の一覧の、いちばん上に、削除された関数が並んでいた。createLabel。getRates。cancelLabel。
先輩が去年組んだ、発送のラベルの処理を思い出した。いま、ちゃんと動いている処理だ。ワインの注文が入るたびに、毎日ラベルを作っている。あの処理は、createLabel と getRates を使っている。
上げれば、いま動いているほうが落ちる。今夜のうちに。
どれだけの数のファイルが、その二つを使っているのか、数えようとして、やめた。数えなくても、落ちることは分かった。数えている時間は無い。
目の前の房を見た。図に合わせて畑を変えるなら、摘む前の木を抜くことになる。
「……合わせる向きが、逆でした」
わたしは、自分に言い聞かせるように言った。
「ページに合わせて、入っているほうを変えるんじゃなくて。入っている実物に合わせて、AI に渡すほうを作る。実物から、作る」
鷹宮さんは、持ち上げていた房を、そっと元に戻した。それから立ち上がって、次の畝のほうへ歩いていった。頷きはしなかった。でも、止めもしなかった。
わたしは、コンテナを数え終えてから、蔵の前のベンチへ移った。蔵の扉が開いていて、中から冷えた空気と、木の樽の匂いが流れてくる。日陰で、パソコンの画面がよく見えた。
調べると、今朝の困りごとには、名前の付いた手当てがあった。最新ドキュメントを渡す、という。AI に、記憶や推測で書かせない。いま手元で動いている実物から取った情報、つまりドキュメントや API の一覧を、コンテキストに入れて渡す。
ここでいう最新は、世の中でいちばん新しい版のことではない。手元で動いている版にとっての、いまの実物どおりの情報のことだ。昨日のわたしは、その言葉の「渡す」だけをやっていた。
渡し方は、いくつかある。ページを貼る。URL を渡して、AI に読みに行かせる。MCP を使う。MCP は、AI のツールに、外のデータや道具をつなぐための、決まった口のことだ。この口につないで、ライブラリのドキュメントを返してくれるサーバーもある。
その一つに、Context7 というものがある。説明のページ(README)には、こう書いてあった。
To get documentation for a specific library version, just mention the version in your prompt:
特定の版のドキュメントがほしいときは、頼む文の中に、その版を書けばいい。版を選ぶことはできる。ただ、プロジェクトの lockfile を読んで、入っている版に自動で合わせる、という記述は見つからなかった。版を書くのは、頼む人だ。
ページを貼るのでも、URL を渡すのでも、MCP でも、渡すのは、誰かが書いた資料だ。どの版の資料かを、わたしが選び間違えれば、また今朝と同じことになる。
そこまで考えて、わたしは手を止めた。
それなら、サイトの選択欄を v5 に切り替えて、ページを貼り直せば済むのではないか。
たぶん、今夜は、それで通る。それは認める。でも、と思った。昨日わたしが滑ったのは、まさに、版を選ぶところだった。それに、そのページが、入っている版の名前を全部載せているかどうかは、ページを見ても分からない。書き忘れがあっても、ページの側からは気づけない。来年、SDK を上げたら、同じ選択が、また来る。
だから、入っている実物から作る。版を選ぶ手順そのものを、無くす。引き継ぎのメモのように、人が書いて渡すものではない。材料は、node_modules に入っている依存そのものだ。人は書かない。機械が、実物から取ってくる。
まず、依存を読み込むところを書いた。
| |
await import() は、プログラムを動かしている途中で、モジュールを読み込む書き方だ。読み込むと、そのモジュールが外に出している名前が全部入ったオブジェクトが返ってくる。これを名前空間と呼ぶ。名前空間の中では、export の名前がそのままキーになっている。
ただ、import('hakobi-sdk') と名前だけ渡すと、Node は、この関数を書いたファイルの位置から node_modules を探しに行く。テストでは、一時的なディレクトリに置いた見本のパッケージから読みたい。ほかのプロジェクトの依存も読めるようにしておきたい。だから、どこの node_modules を読むかを、dir で外から渡せるようにした。そこで、createRequire を使う。指定したディレクトリの package.json を起点にして、パッケージの探し方を作る関数だ。その resolve で、dir の node_modules にある hakobi-sdk の、本体のファイルの絶対パスを得る。createRequire・join・pathToFileURL、あとで使う readFileSync は、どれも Node に最初から入っている関数だ。戻り値の型 Promise<Record<string, unknown>> は、文字列のキーに何かの値が入ったオブジェクトを、あとで渡すという約束、くらいに読めばいい。pathToFileURL で、そのパスを import() が読める URL の形に直してから、読み込む。
名前空間のキーを、そのまま並べる関数も書いた。
| |
Object.keys は、オブジェクトのキーを配列で返す。sort は、並べ替える。並びを毎回同じにしておきたかった。
入っている hakobi-sdk で走らせた。
| |
createLabel がある。getRates も、cancelLabel もある。createShipment は無い。
ほっとしかけて、二つの名前に目が止まった。default。module.exports。
はこび便の SDK に、そんな関数は無い。v5 のページにも、v6 のページにも無い。
わたしが AI に渡そうとしていた資料に、無い名前が入りかけていた。
思わず、小さく笑ってしまった。今朝から、無い名前を書かれて困っていたのに、自分の作った資料が、同じことをしようとしていた。これをそのまま渡したら、AI が default という関数を呼ぶコードを書いても、文句は言えない。
調べると、理由はモジュールの書き方にあった。Node のモジュールには、古くからある CommonJS(CJS)と、今の標準の ES Modules(ESM)の二つの書き方がある。はこび便の SDK は、CJS で書かれている。CJS のモジュールを import() で読むと、名前空間に、default と module.exports という二つのキーが足される。どちらも、モジュール全体を指すための入り口で、SDK が外に出している関数の名前ではない。CJS の名前のほうは、Node がファイルを実行せずに、exports.名前 = のような字面から拾って並べている。実行してみないと決まらない書き方をしていると、名前が出てこないこともある。
テストを書いた。
| |
! は、真と偽をひっくり返す。名前に default が含まれていなければ通る。素朴な関数では、これは赤だった。二つを除くように直した。
| |
filter は、条件に合うものだけを残す。これで、hakobi-sdk の名前は六つになった。
そこで、もう一つの依存のことを思い出した。明日の予約の画面で、空いている枠を出しているのは、kakeru-calendar という予約カレンダーの部品だ。明日いちばん使われるのは、たぶん、こっちだ。こっちの資料も作っておこうと思った。
走らせると、名前が一つも出てこなかった。
空の配列だった。openSlots も、reserve も無い。
kakeru-calendar の中を開いた。ESM で書かれていて、最後の一行が、こうなっていた。
| |
export default は、名前を付けずに、一つだけ外に出す書き方だ。この部品は、三つの関数を一つのオブジェクトにまとめて、それだけを出している。だから、名前空間のキーは default だけになる。わたしは、さっきその default を除いた。除いたら、何も残らなかった。
default は捨てるだけでなく、中を見る必要があった。テストを足した。
| |
assert.deepEqual は、配列の中身まで同じかを確かめる。二つを除いただけの関数では、これが赤になる。default の中身のキーを足すように直して、名前を取り出す関数を仕上げた。
| |
typeof は、値の種類を文字列で返す。default がオブジェクトか関数なら、その中のキーを足す。null も typeof では 'object' になるので、別に除いている。CJS では、モジュール全体が一つの関数になっていることもあるので、関数も見ている。push(...配列) は、配列の中身を一つずつ足す書き方だ。CJS の SDK は、default の中にも同じ名前が入っているので、名前が二回ずつ出てくる。new Set は、重複を除いた集まりを作る。それを [...] で配列に戻して、並べる。
二つの依存で、名前空間のキーがどう残っていくかを、並べて書いておいた。

どちらの依存でも、最後に残ったのは、入っている版が外に出している名前だけだった。
次は、版だ。入っている依存の package.json の version を読めばいい。最初は、さっきと同じ resolve で hakobi-sdk/package.json を探そうとした。すると、ERR_PACKAGE_PATH_NOT_EXPORTED というエラーが出た。パッケージの package.json には、外から読んでいいファイルを決める exports という欄を書ける。はこび便の SDK は、そこに本体のファイルしか書いていない。だから、package.json は外から探せない。
それなら、置き場所が分かっているのだから、直接読めばいい。
| |
readFileSync でファイルを文字列として読み、JSON.parse でオブジェクトに直して、version を取る。ponytail: で始まるコメントは、わざと手を抜いたところの印だ。この関数は dir のすぐ下の node_modules しか見ない。依存を別の場所に置く道具を使っている構成や、親のディレクトリの node_modules に入っている依存は読めない。うちのサイトは、依存がすぐ下にあるので、これで足りる。
貼ったページの資料で赤だった三本を、今度はこの関数で作った資料に向けて走らせた。三本とも緑になった。
最後に、もう一つ確かめておきたいことがあった。
v5 のページを貼っていたら、どうだったか。
| |
renderNotes は、資料を AI に渡す Markdown に書き出す関数で、中身はあとで見せる。書き出した文字列まで同じになるかも、ついでに確かめている。DOC_V5 は、選択欄を v5.3.0 に切り替えたときのページ、DOC_CALENDAR は予約カレンダーの部品のページだ。どちらも、入っている版のページで、入っている版の名前を全部載せている。そういうページから作った資料と、実物から作った資料は、名前も版も同じになるはずだ。これも緑だった。
ページが入っている版のものなら、貼っても同じだった。違いが出るのは、合っていないときだけだ。実物から作るやり方は、合っているときの答えを変えない。
昨日のわたしは、AI の覚えていることより、公式のページのほうが新しいと思って貼った。それは、半分は合っていた。足りなかったのは、ページの版を、入っている実物と合わせることだった。合わせる向きは、実物から。
第4幕: 検める ── また歩く
昼すぎ、蔵の前のベンチで、資料を作るためのスクリプトを書いた。
| |
ここまでの関数は after.ts に、資料の型と書き出しは notes.ts にまとめてある。process.argv[2] は、コマンドに付けて渡した一つ目の値だ。process.cwd() は、いまいるディレクトリ。process.exit(1) は、失敗したことを呼び出し元に知らせる終わり方だ。ESM のファイルなら、いちばん外側でも await を書ける。資料を Markdown にするのは、この関数にした。
| |
最後の一行は、書いているうちに足した。この資料には、名前と版しか入らない。関数に何を渡して、何が返ってくるかは、入らない。それを、資料を読む側に、正直に伝えておきたかった。index.d.ts は、パッケージに入っている型の定義のファイルだ。引数や戻り値の形は、そこに書いてある。
package.json の scripts に、一行足した。npm run で呼べるコマンドに名前を付けておく欄だ。
| |
npm run api-notes を走らせると、API-NOTES.md ができた。
| |
貼ったページに比べると、拍子抜けするほど短い。でも、ここに書いてある名前は、全部、入っている版に実在する。入っている版の名前は、全部ここにある。版は 5.3.0 だ。
テストを全部走らせた。
| |
全部、緑だった。最後の一本は、スクリプトを本当に別のプロセスとして走らせて、出てきた文字列が、上の API-NOTES.md と一字も違わないことを確かめている。テストの中では、本物の node_modules の代わりに、一時的なディレクトリに hakobi-sdk と kakeru-calendar の見本のパッケージを書き出して、そこから読んでいる。見本の形は、本物の SDK と同じ CJS と、カレンダーと同じ ESM に合わせてある。
この資料を、AI にどう渡すか。ここからは、2026年10月22日時点の Claude Code の書き方だ。Claude Code の公式の手引きには、ファイルの渡し方について、こう書いてある。
Reference files with
@instead of describing where code lives. Claude reads the file before responding.
コードの場所を言葉で説明するより、@ を付けてファイルを指せ。Claude は、答える前にそのファイルを読む。わたしは、頼む文に @API-NOTES.md を付けることにした。
| |
指示書のファイル(CLAUDE.md)の中に @API-NOTES.md と書いて、取り込ませる手もある。ただ、公式の説明によれば、指示書に取り込んだファイルは、起動のたびに全文が読み込まれる。毎回コンテキストを使うし、作った日の資料が、黙って毎回渡されることになる。頼むときに、頼む文で渡す。
MCP で渡すなら、プロジェクトで共有するサーバーは、リポジトリのいちばん上の .mcp.json に登録する。claude mcp add に --scope project を付けて追加すると、ここに書かれる。ドキュメントを返すサーバーを使うなら、頼む文に版を書く。書き忘れれば、サーバーは、たぶん新しいほうを返す。わたしの資料は、入っている版から機械が作るので、版を書き忘れることが無い。
AI に、@API-NOTES.md を付けて、発送の処理を入っている版で書き直すよう頼んだ。AI は、createShipment を createLabel に、quoteRates を getRates に置き換えて、schedulePickup を使っていたところは、集荷の日時を画面で選ばせるのをやめる形に直した。ここに書いたのは、今日のわたしの頼みに対して返ってきたものだ。同じ資料を付けて頼み直しても、毎回この書き方になるとは限らない。
ビルドを走らせた。
TS2305 は、一つも出なかった。存在しない名前のエラーは、全部消えた。
エラーは、三つ残っていた。三つとも、createLabel に渡しているオブジェクトの、項目の名前が違う、というものだった。v6 の書き方の項目名が、そのまま残っていた。
資料には、名前しか無い。どんな形のものを渡せばいいかは、資料の最後の一行が指している index.d.ts に書いてある。わたしは、AI に index.d.ts を読ませて、三つを直してもらった。もう一度ビルドを走らせると、今度は通った。16時40分だった。
ビルドが通るまでの流れを、昨日の夜の流れと並べて、ノートに書き出してみた。

決まった手順で作れるのは、資料ができるまでだ。AI が書くところと、それを確かめるビルドは、昨日と同じく別にある。
坂の上の蔵へ行って、葛城さんに伝えた。18時までに、公開の作業を終えられます。明日の10時、予約、始まります。葛城さんは、帽子を脱いで、よかった、と言った。
日の入りの少し前、葛城さんと二人で、畑の端に出た。
空が、橙色から薄い紫に変わりかけていた。丘の向こうから、小さな黒い点が、ぱらぱらと戻ってきた。朝ほどではないけれど、群れだった。何羽かが、C の木の列の葉の陰に降りた。
鷹宮さんは、拳を上げる前に、畝を一本、下から上まで歩いて、木の列の端で立ち止まった。それから、拳を上げた。
鷹が飛ぶと、群れはまた上がった。でも、全部は丘を越えなかった。十羽ほどが、隣の畑の電線にとまって、こちらを見ていた。
「明日の朝も、来ます」
戻ってきた鷹を拳で受けながら、鷹宮さんは葛城さんに言った。葛城さんは、助かる、と言って笑った。
わたしは、電線の鳥を見ていた。
テストで確かめたのは、資料の中身までだ。資料に載る名前は、入っている版に実在するものだけで、入っている版の名前は全部載る。版も、入っている版のものになる。ページが入っている版のものなら、ページから作った資料と同じ中身になる。
AI がそれで間違えなくなる、とは言えない。ビルドに残った三つが、その一つだ。資料が伝えるのは名前と版だけで、引数も、型も、使い方も伝えない。そこは、index.d.ts と、tsc のビルドで確かめるしかない。AI は、資料に無い名前を、推し量って書いてくることもありうる。資料は AI に渡す入力で、AI の出したものを弾く仕掛けではない。
@API-NOTES.md を付け忘れれば、何も変わらない。読むのは dir のすぐ下の node_modules の依存だけで、CJS の名前は Node が静的に見つけられたものまでしか取れない。
もう一つ。七本の緑が言っているのは、わたしが手で作った見本のパッケージを、わたしの関数がどう読むか、だけだ。Node のモジュールの読み方や、Claude Code の @ の扱いが改訂で変われば、この仕掛けは、黙って効かなくなりうる。今日の緑は、今日の Node と今日の Claude Code での緑だ。
畝の上のほうから、鷹宮さんが下りてきた。拳の上で、鷹が首を小さく回した。
「鷹宮さん。もう一つだけ、すみません」
わたしは、聞いておきたいことがあった。
「来年、SDK を上げたら……今日作った資料は、どうなるんでしょう」
鷹宮さんは、少しだけ首をかしげた。それから、C 区画のほうを見た。来年の春には、メルローの木が抜かれて、若いシャルドネの苗が植わっているはずの区画だ。
「来年は、来年の C を、また歩きます」
それだけだった。
わたしは、それを自分の言葉で受けた。来年 SDK を上げたら、上げた日に、上げた版から、同じ作り方で、また作る。作るのは、わたしではない。npm run api-notes が、入っている実物から作る。わたしがやるのは、上げたあとに、その一行を走らせることだけだ。走らせるのを忘れたら、資料は今日の版のまま残る。そこは、仕掛けでは防げない。
「あの……今日のこと、何か書いてもらえませんか。すみません。春に、この仕事を先輩から引き継いだので。次に誰かに引き継ぐとき、見せたくて」
鷹宮さんは、少し黙った。それから、葛城さんに、何か書くものはありますか、と聞いた。葛城さんは、直売所の机から、ワインの箱に付ける荷札を一枚持ってきた。紐の付いた、厚紙の札だ。
鷹宮さんは、荷札を防鳥ネットの支柱に当てて、ボールペンで書いた。そのあいだ、鷹は、支柱の上でじっとしていた。四行、書いたように見えた。書き終えると、鷹宮さんは、荷札をわたしに渡した。
「どうぞ」
それから、拳に鷹を据えて、畝の奥へ歩いていった。
わたしは、車に戻った。後ろのハッチバックを上げると、荷室の灯りが点いた。ワインの箱が二つ積んであった。明日の予約の、最初のお客さんに送る分だ。クール便で、と葛城さんに頼まれている。
灯りの下で、荷札を読んだ。四行の下が、一行ぶん空いていた。
バッグの底からペンを探し出して、荷室の床に荷札を置き、その一行を埋めた。
放鷹カード
- 合図: 最新ドキュメントを渡す。入っている依存から、名前と版を取り出して資料を作る。頼むときに、その資料を付ける
- 検め方: 資料の版が、入っている版と同じか。資料の名前が、入っている版に実在するか。引数と型は、
index.d.tsとtscで - 止めないもの: ページが入っている版のものなら、ページから作ったのと同じ資料になる。入っている版の名前は、一つも削らない
- 鷹のせいじゃなかった: 公式のページを、入っている版と比べずに渡した。ページに合わせて、入っているほうを上げようとした
- 自分の言葉で: AI に渡す資料は、ページからではなく、node_modules から作る。伝わるのは名前まで。形は index.d.ts とビルドで見る
