日本語 · English(未訳)

2026-10-31 機械レビュー(PR #9=1.1.0 のカレンダー差分・8 件)

処置の記録(2026-10-31)

# 重さ 処置 検査
1 MEDIUM hasExample(lang, id)=Object.hasOwn で例を判定・defaultValues/sanitizeValues/packValues/unpackValues/buildSource は exampleOf を通し無い id は明示エラー・起動の分岐全体を try で包み失敗は最初の例へ witness: hasExample('ja','constructor')=false・unpackValues('ja','constructor','') が throw
2 MEDIUM isValidTz(tz)(Intl で 1 行検査)。窓の計算(selectExample・起動の Playground 由来)は不正なら既定 tz で取り、評価器の「不正な tz」の文言は従来どおり黄枠に witness: isValidTz('Asia/Tokyoo')=false
3 MEDIUM wkstOf=@ 行は /^\s*@(\w+)\b/m(註・字下げを許す)・premise Name = Base with {…} の連鎖を基底まで辿る(深さ 10)・「ソース中の最初の wkst:」の代用は廃止(wkst の無い premise は Mon) witness: 所見の再現 3(A Sun・B Mon・P = B with)=Mon・@B # 在圏=Mon・字下げ @A=Sun・wkst 無しの X=Mon
4 LOW 年末年始の項を yearEnd = everyDay \|> within(year) \|> last・newYear = … \|> nth(1) と shift(±1, ±2, unit: day) の和に(実測 650〜1,650 ms → 20 ms・同じ 6 点)。debounce は入れない(change は離散) witness: lesson の既定と変種が CLI と同じ点列(既存)
5 LOW 表の前後両方にはみ出すときは starts と ends の 2 文をつなぐ witness: 2025-12-01..2028-02-01 で「2025 年」と「2028 年」の両方
6 LOW withName は改行を空白に witness: withName('# a\nx', 'b\neveryDay')=# b everyDay\nx
7 LOW sanitizeValues の witness を追加 unpackValues('ja','garbage','9,x,Foo')=既定値・('ja','alarm','25:00,7:30')=既定値
8 LOW WD_SHORT_EN と renderForm の lang 引数を削除 typecheck・979

所見(投稿どおり・逐語)

機械レビュー所見(PR #9・1.1.0 のカレンダー差分)

対象: base 2cc9691 → head 6d61138(19 ファイル・+1114/−28)。検査は head の作業木で npm run typecheck(OK)・npx vitest run(979/979・102 s)・TZ=UTC npx vitest run(979/979・78 s)・node scripts/check-test-count.mjs(979=入口 6 箇所 OK)。所見はすべて最小入力で赤を実測したもの。枝には何も push していない。

# 重さ 場所(ファイル:行) 何が(期待と実測) 再現(コマンドか入力) 直し方の案
1 MEDIUM calendar/core.js:673(起動の EX[h.e])・:202 URL の e に constructor・__proto__・toString など Object.prototype の名前が来ると EX[h.e] が真になり、unpackValues が EXAMPLES[lang][id].fields で TypeError。期待: 知らない e は無視して最初の例に戻る。実測: 起動が途中で止まり、穴も格子も出ない(月 0・穴 0・cal-src 空) /calendar/#e=constructor&f=2026-10-01&t=2026-11-01(#e=__proto__ も同じ)。純関数なら node -e で unpackValues('ja','constructor','') → TypeError: Cannot read properties of undefined (reading 'map') Object.hasOwn(EX, h.e) で判定(defaultValues/unpackValues の入口にも同じ番を置く)。起動の分岐全体を try で包み、失敗したら selectExample(Object.keys(EX)[0]) に落とす
2 MEDIUM calendar/core.js:670・:471(monthsWindow/rollingWindow の Intl) URL の z が不正な IANA 名で f・t が無いと、selectExample の窓計算で Intl.DateTimeFormat が RangeError。期待: 不正な tz は run の KairosError と同じく黄枠に出て描画は続く(f・t 付きの URL ではそうなる)。実測: 起動が止まり何も描かれない。tz 欄に不正な名前を入れてから例を切り替えても同じ例外(select は pay に変わるが穴と式は garbage のまま) /calendar/#z=Asia/Tokyoo → Invalid time zone specified: Asia/Tokyoo。操作: tz 欄に Asia/Tokyoo → 例を切替。純関数なら monthsWindow('Asia/Tokyoo', 12) tz を使う前に 1 行で検査する(try { new Intl.DateTimeFormat('en-CA', { timeZone: tz }) } catch { tz = defaultTz(lang) })を readRange と起動に置く。または selectExample の窓計算を try で包み、失敗時は既定 tz で窓を取り黄枠に run と同じ文言を出す
3 MEDIUM calendar/core.js:255-265(wkstOf) (a) @P が派生 premise(premise P = B with { … })で wkst を基底から継承していると P のブロックに wkst: が無く、「ソース中の最初の wkst:」に落ちて別の premise の値を拾う。(b) @B # 註 や字下げした @B は /^@(\w+)\s*$/m に掛からず同じ代用に落ちる。期待: 評価器と同じ週の始まり。実測: A が wkst: Sun・B が wkst: Mon・premise P = B with { tz: "Asia/Tokyo" }・@P で wkstOf → Sun、評価器の everyDay \|> within(week) \|> nth(1) は 2026-10-05 Mon・2026-10-12 Mon。ブラウザの見出しの先頭も「日」。@B # 在圏 でも同じ 下の「再現 3」のソースを s= に載せて開く。純関数なら wkstOf(src) と run(src, …) を並べる @ 行は /^\s*@(\w+)\b/m で読み、premise Name = Base with {…} の連鎖を基底まで辿って最初に見つかる wkst: を取る。見つからなければ Mon。「ソース中の最初の wkst:」の代用は外す(当たるのは偶然)。評価器に聞ける口(run が返す runtime から在圏 premise の wkst を出す)があれば正規表現を持たない方が確か
4 LOW calendar/core.js:161(lessonBody)・:153(alarmBody)・update→render 穴を 1 つ変えるたびに本体式と祝日式を評価して 12 か月の DOM を組み直す。実測(Node・12 か月・温まった後の 3 回): garbage 114–127 ms・burnable 151–166・pay 180–183・alarm(28 日窓)620–964・lesson 967–1317 ms。ヘッドレスのブラウザ内で穴を 1 回変える所要: lesson 688–854 ms・garbage 702 ms。lesson の年末年始の項 everyDay \|> filter(d => (month(d) == 12 and dayNo(d) >= 29) or (month(d) == 1 and dayNo(d) <= 3)) が単独で 562 ms(6 点) 下の「再現 4」 年末年始を ye = everyDay \|> within(year) \|> last・ny = everyDay \|> within(year) \|> nth(1) と shift(±1, unit: day)・shift(±2, unit: day) の和で書くと同じ 6 点で 32 ms(実測)。8 月の month(d) == 8 は 256–270 ms で残るので、必要なら穴の change を 150 ms 程度 debounce する
5 LOW calendar/core.js:319-320(plainWarning) 範囲が表の前後両方にはみ出すと after 側しか言わない。期待: 両方の年を知らせる。実測: from 2025-12-01 to 2028-02-01(表は 2026..2027)で註釈は 2025-12-01..2026-01-02 と 2028-01-01..2028-02-01 の 2 本だが、一言は「この表は 2027 年分までです。2028 年の予定は祝日を知らずに出ています。」で 2025 年が出ない plainWarning('ja', run(buildSource('ja','pay',…), { from: '2025-12-01', to: '2028-02-01', tz: 'Asia/Tokyo' }).results[0].annotations, …coverage) after と before の両方があれば ends と starts の 2 文をつなぐ
6 LOW calendar/core.js:330(withName) name に改行が含まれると式の行が増える(「コメント行だけを差し替える」の外)。実測: withName('# a\nx', 'b\neveryDay') → # b\neveryDay\nx(nameOf は b を返し、everyDay が本体式に加わる)。UI の input type=text から改行は来ないので影響は無いが、export された純関数で検査もあるので契約として閉じておきたい node -e で上の呼び出し const n = String(name ?? '').replace(/[\r\n]+/g, ' ').trim()
7 LOW impl/test/calendar.test.ts:65(往復の検査のみ) sanitizeValues の「選択肢にない値・時刻でない値は既定値に戻す(URL から来た値を信じない)」に witness が無い。変異(select と time をそのまま通す)で calendar.test.ts 6 本とも緑のまま core.js:187-189 の 2 行を out[f.key] = v ?? f.def に変えて npx vitest run test/calendar.test.ts → 6 passed unpackValues('ja','garbage','9,x,Foo') → 既定値、unpackValues('ja','alarm','25:00,7:30') → 06:30/08:00 の期待を 1 本足す
8 LOW calendar/core.js:16・:402 WD_SHORT_EN は未使用。renderForm の第 1 引数 lang も未使用 grep -n WD_SHORT_EN calendar/core.js → 定義 1 件のみ 削る

再現の詳細

再現 3(wkstOf と評価器の不一致。s= に base64url で載せても、純関数を直接呼んでも同じ):

premise A { calendar-system: Gregorian; tz: "Asia/Tokyo"; wkst: Sun }
premise B { calendar-system: Gregorian; tz: "Asia/Tokyo"; wkst: Mon }
premise P = B with { tz: "Asia/Tokyo" }
@P
everyDay |> within(week) |> nth(1)

wkstOf → Sun。run(src, { from: '2026-10-05', to: '2026-10-19', tz: 'Asia/Tokyo' }) → 2026-10-05・2026-10-12(月曜)。3 行目を外し 4 行目を @B # 在圏 にしても wkstOf → Sun・評価器は月曜。

再現 4(年末年始の項の 2 形・JP_PREMISE+@JP・2026-10-01..2027-10-01・Asia/Tokyo・温まった後):

# 562 ms・6 点
everyDay |> filter(d => (month(d) == 12 and dayNo(d) >= 29) or (month(d) == 1 and dayNo(d) <= 3))
# 32 ms・同じ 6 点(2026-12-29 … 2027-01-03)
ye = everyDay |> within(year) |> last
ny = everyDay |> within(year) |> nth(1)
ye | (ye |> shift(-1, unit: day)) | (ye |> shift(-2, unit: day)) | ny | (ny |> shift(1, unit: day)) | (ny |> shift(2, unit: day))

takeLast(3)/take(3) の形は「窓付き入力は取らない」(ADR-52)で止まるので使えない。

確認したが所見にしなかったもの(根拠つき)

読んだ順と掛けた時間

  1. 指示書・MAINTENANCE.md・PR 本文
  2. 差分全文: calendar/core.js → calendar/index.html・en/calendar/index.html → playground/core.js・playground/index.html(日英)→ impl/test/calendar.test.ts・boundary-checklist.test.ts → README 日英・en/spec・llms.txt・impl/README・recipes/README 日英・CHANGELOG
  3. 検査 3 本(typecheck・vitest JST/UTC・check-test-count)を head の作業木で
  4. 純関数の再現(node で calendar/core.js・playground/core.js・playground/js/index.js を直接 import): 境界の総当たり・URL の値・wkst・tz・性能
  5. ヘッドレスのブラウザで /calendar/・/en/calendar/ を静的配信して起動時の例外と描画を実測
  6. 変異 1 本(sanitizeValues)

掛けた時間: 約 40 分(検査の待ち時間を含む)。