Updates

更新

Gemini News Generators

Gemini APIとGitHub Actionsを利用して、技術・政治経済・エンタメのニュース記事を生成する自動化ジョブ群。

Geminiニュース生成ジョブ3件の定期実行を停止

ciリリース済み

Geminiニュース生成ジョブ3件の定期実行を停止

リリース状況

リリース済み(2026-10-16)。3つのGitHubリポジトリのデフォルトブランチ master に変更を反映し、ワークフローファイルを再取得して schedule が存在しないことを確認した。次回以降の定期起動がなくなる構成変更であり、過去に開始済みのワークフローのキャンセルや、次回実行時刻経過後の実地検証は行っていない。

何を変えたか

  • gemini-tech-news、gemini-politics-economy-news、gemini-entertainment-news の各 .github/workflows/daily.yml から on.schedule を削除した。
  • 手動実行の workflow_dispatch と既存のジョブ、プログラム、記事は保持した。
  • 変更後、3件とも「schedule なし・workflow_dispatch あり」を再取得で検証した。

なぜ変えたか

使われていない定期ジョブがGitHub Actionsの月間無料実行枠を消費し続けるため、ジョブの廃棄ではなく自動起動だけを止める方針とした。

利用者にとって何が良くなったか

不要な毎朝のジョブ起動による無料枠消費を防ぐ。過去のコードと出力を保ち、再利用時はActions画面から手動実行できる。

技術的な内容

  • 変更前: on.schedule に cron: '0 22 * * *'(UTC 22:00、日本時間翌7:00)と workflow_dispatch が併存。
  • 変更後: workflow_dispatch のみ。GitHub Actionsでは on に宣言されていないイベントではワークフローを起動しない。
  • schedule をコメントアウトまたは削除しても、手動実行条件を残せばジョブ定義を保ったまま休止できる。
  • 今回は設定トリガーだけを変更し、生成・Git pushなどの処理ロジックは変更していない。

判断とトレードオフ

  • GitHub Actionsそのものを無効化すると復旧時に設定操作が必要になるため、定期実行の停止にとどめた。
  • 手動実行は可能なので、完全に実行不可能にする方式ではない。
  • この変更で過去に消費した利用分数が払い戻されるわけではない。

ハマりどころと学び

  • Actionsの無料枠は同一所有アカウント配下の非公開リポジトリで合算される。利用量削減ではリポジトリ間の定期ジョブも棚卸し対象になる。
  • スケジュール削除だけでは「既にキューに入ったジョブ」の処理を遡って取り消せないため、ワークフロー変更の検証と実行中ジョブの確認は分けるべき。
  • 定期ジョブを止めるときは workflow_dispatch を残しておくと、将来の保守・原因調査をしやすい。

GitHubプロフィールREADME

技術スタック、個人開発の実績、公開技術記事、GitHub活動を1か所に集約する公開プロフィール。

公式ページ

GitHubプロフィールREADMEと活動・記事自動更新の実装準備

feature未リリース

GitHubプロフィールREADMEと活動・記事自動更新の実装準備

リリース状況

未リリース(2026-10-16時点)。README・GitHub Actionsワークフロー・Node.js更新スクリプトをローカルで作成し、RSS更新の単体テスト4件に成功した。 GitHubユーザー名と同じ公開リポジトリがまだ作成されておらず、GitHubへのコミット・プロフィールでの表示・Actionsでの実行成功は未確認。

何を変えたか

  • プロフィール用READMEに自己紹介・使用技術・個人開発・技術記事・活動欄を構成した
  • Snakeアニメーションのlight/dark SVGを毎日生成してoutputブランチに配信するワークフローを作成した
  • 技術記事RSSから最新3件を抽出し、README内の指定マーカー範囲だけを更新するスクリプト・定期ワークフローを作成した
  • フィードの欠損、更新マーカーの欠損・重複、外部ドメイン混入、URL重複を検査する単体テストを作成した

なぜ変えたか

複数の制作物・技術記事をプロフィールで一覧できるようにしつつ、活動と記事更新を手動編集なしで反映するため。

ユーザーにとって何が良くなったか

公開後の想定効果: 実績の確認先が一か所になり、技術記事と活動画像が自動で更新される。 未公開のため、現時点でプロフィール訪問者向けの改善が実際に提供されたという意味ではない。

技術的な内容

  • RSSは https://chicchi.work/articles/rss.xml を参照予定
  • JavaScriptはNode.js標準のfetch・fsを使用し、パッケージ依存を追加しない
  • 記事リンクはHTTPS、固定ドメイン、/articles/<slug>/ の形式を検証し、ニュースURLや外部ドメインを排除
  • Markdownの表示を壊す文字はエスケープしてから記事リンクを組み立てる
  • READMEに更新マーカーがちょうど1組ある場合のみ記事欄を置換
  • フィード取得やパースに失敗すると処理は非ゼロ終了し、既存READMEを変更しない
  • Actionsのジョブ権限はcontents: writeに限定し、外部Actionは2026-10-16時点のcommit SHAに固定
  • Snakeの生成結果はoutputブランチ、READMEはmainブランチとして分離

Before / After

Before

プロフィールに専用READMEがなく、公開実績・記事への統一された導線も自動更新処理もなかった。

After

公開用のファイル群が準備された。ただし、実際のGitHubプロフィールへの反映は未実施。

判断したこと・トレードオフ

  • 技術記事をスクレイピングせず公式のRSSを使用し、定期更新の負荷と実装を小さくした
  • RSS取得に失敗した場合は空の一覧を公開するのでなく、既存の選定記事を保持する
  • Snakeの出力専用ブランチへの保存により、生成によるREADMEの更新競合を避ける
  • GitHubプロフィール専用のリポジトリ作成が必要。接続済みのGitHubツールでは新規リポジトリ作成操作がないため、公開まで到達できなかった

ハマったこと・学んだこと

  • ユーザー名と一致する公開リポジトリがないと、他の公開リポジトリにREADMEを置いてもプロフィール上部には表示されない
  • RSSのURLを無条件にMarkdownに転記すると、外部URLやニュースが技術記事一覧に紛れるため、厳密なドメイン・パス検証が必要
  • 画像生成ワークフローの初回成功までraw.githubusercontent.comの出力画像は存在せず、READMEが画像切れになる可能性がある
  • GitHub Actionsは実装がローカルテストに成功しても、ワークフロー権限やブランチ書き込みの制約を本番の実行結果で確認する必要がある

プロフィールREADMEの公開とRSS・Snake自動更新の実行確認

releaseリリース済み

プロフィールREADMEの公開とRSS・Snake自動更新の実行確認

リリース状況

リリース済み(2026-10-16)。同名の公開リポジトリのmainへREADME・2つのワークフロー・RSS更新プログラム・テスト・保守文書を反映した。

GitHub Actions上で技術記事RSSの取得、最新3件へのREADME更新とpush、Snakeのライト・ダークSVGの生成とoutputブランチへの保存が成功した。公開リポジトリのREADMEとoutputブランチに生成物が存在することをGitHub APIから再取得して確認した。

確認範囲は公開ファイルとGitHub Actionsの実行結果。各ブラウザの実画面やGitHub画像キャッシュの反映状態の目視検証は含まない。アカウントのBio、アバター、ピン留め、他リポジトリの公開設定は変更していない。

以前の未リリース記録は経緯として残し、この追記を現行の公開状態とする。

何を変えたか

  • 自己紹介、技術スタック、制作物、記事、活動を1列で読めるプロフィールREADMEを公開した。
  • 最新技術記事の更新を毎日11:27 JST、Snakeの更新を毎日10:43 JSTに設定した。いずれもGitHub Actionsでの実行予定時刻であり、混雑時などに遅延する場合がある。
  • 単体テストを4件から8件へ拡充し、HTML文字の無害化、掲載件数上限、不正なURL、空の記事一覧を追加検証した。
  • RSS取得失敗や不正データの場合は既存のREADMEを変更しない。
  • Action実行ログで見つかったNode.js 20の非推奨警告を受け、checkoutとsetup-nodeをNode.js 24対応のv6系へ更新した。

なぜ変えたか

ローカルでファイルを用意するだけではプロフィール訪問者に届かず、ワークフローの権限や外部通信も検証できなかった。公開リポジトリへの反映後に、実際のGitHubホスト環境で取得・生成・書き込みまで確認する必要があった。

ユーザーにとって何が良くなったか

制作物と記事への導線をGitHub上でまとめて確認できる。記事や活動画像の手動更新が不要になった。外部フィード取得に失敗しても、既存の記事一覧が空になることを防ぐ。

技術的な内容

  • READMEをmain、生成したSVGをoutputへ分離し、2つの処理が同じブランチへ書き込む競合を避ける。
  • 外部Actionは40文字のコミットSHAで固定する。
  • contents: write とリポジトリ標準のGITHUB_TOKENを利用し、個人用トークンやAI APIキーを追加しない。
  • 記事URLはHTTPS、固定ドメイン、記事パス、認証情報・クエリ・フラグメントなしを検査する。
  • 記事タイトルのHTML特殊文字とMarkdown記法をエスケープし、表示用テキストとして扱う。
  • README更新前にマーカーが1組だけ存在することを検査し、その範囲だけ置換する。
  • pushはforceを使わず、必要に応じてrebaseで先行変更を取り込む。競合時は処理を失敗させて既存のリモート履歴を保護する。

Before / After

Before

READMEと自動更新コードはローカルのみで、プロフィール専用リポジトリへの反映と実行確認が未完了だった。

After

公開リポジトリに反映し、RSSから取得した3記事への更新コミットと、2種類のSnake SVGの公開を確認した。2回目の記事更新実行も成功した。

判断したこと・トレードオフ

  • 個人開発プロダクトのソースコードは公開せず、公開サイトへのリンクと紹介文だけを掲載する。
  • RSSに並ぶ先頭3件を利用する。プロファイル側で独立した並べ替えや記事の内容判定は行わない。
  • 空のデータで記事を消すより、既存の一覧を残してActionsを失敗させる設計を選んだ。
  • 毎日の予定時刻を設定したが、定期実行自体の将来の成功・定刻性までは保証しない。

ハマったこと・学んだこと

アプリで使用するNode.jsとAction自身の実行環境は別

更新スクリプトをNode.js 22で動かしていても、checkoutやsetup-node自身がNode.js 20を要求すると非推奨警告が出る。初回の成功ログに警告があったため、外部Actionをv6系の固定SHAへ更新し、再実行成功を確認した。

ローカルテストだけでは外部通信・書き込み権限を証明できない

ローカルではテスト8件が成功したが、作業コンテナからの外部通信はDNS解決に失敗した。これをサイト障害と断定せず、GitHub ActionsのログでRSS取得とpush成功を確認した。

RSSのタイトルはHTMLにもMarkdownにも意味を持つ

<、>、&や角括弧をそのまま連結すると、見出しがHTMLやリンク構文として解釈される場合がある。表示用エスケープと回帰テストを追加した。

MAD TOMMY WITH…

オリジナルキャラクターと刺繍アイテムを組み合わせて注文できるオンラインストア。

公式ページ

注文・クーポン・通知の整合性修正

releaseリリース済み

注文・クーポン・通知の整合性修正

リリース状況

リリース済み。本番コミット 65798f8a0341a7979bc20d472159af64e27571d8 がApp Hostingから100%配信され、対象10 Cloud FunctionsがACTIVEであること、Firestore Rulesがレビュー済みソースと一致することを確認した。購入を伴わない本番スモーク確認と、メンテナンス設定の復元も完了。詳細な結果は関連PRに記録済み。

何を変えたか

  • クーポン本体の取得を管理者・サーバーへ限定し、顧客には認証付きAPIから必要な情報だけを返す。
  • 決済セッションの世代を注文取消のTransaction内で照合し、古いイベントが新しい決済を取り消さないようにする。
  • 管理画面の状態変更を配送の進行に制限し、金銭処理を伴う状態を単なる文字列変更で更新しない。
  • 注文作成時にサーバーが確定した単価・明細小計・商品名を保存し、商品マスター変更後も購入履歴と分析へ引き継ぐ。
  • 購入・問い合わせのDiscord通知を永続キューに保存し、一時障害後の再送と排他制御を追加する。

なぜ変えたか

現在の商品情報、非同期イベント、外部通知の成否に過去の注文情報や注文状態を依存させると、表示・在庫・決済・運用通知が食い違うため。読み取り経路、保存時点、状態遷移、失敗時の回復をそれぞれ明示した。

ユーザーにとって何が良くなったか

購入当時の金額を確認でき、決済の遅延イベントや管理操作による注文状態の不整合を防ぐ。通知の一時失敗も自動回復の対象になる。

技術的な内容・学び

Firestoreの権限は文書全体に適用されるため、顧客へ返すデータはサーバー側で明示的に投影する。クーポン一覧と入力コードの並行取得では、古い一覧応答が新しい照会結果を消さないようにする。

単価を保存するだけでは不十分で、再取得時のデコーダーが新しいフィールドを落とさないことが必要だった。旧注文の不明価格は現在価格や0円で補わない。

決済の再作成中断は、作成リクエストと冪等キーを保存してSchedulerから回復する。保持期限や時刻の検証は、自動処理と利用者の再開操作で共通化した。情報不足・期限超過時には支払いの可能性を無視して在庫を解放しない。

通知のキューでは期限付きの処理権をTransactionで取得し、古いワーカーによる完了更新も拒否する。外部サービスに冪等性がないため、送信成功後に記録だけが失敗した場合の重複可能性は残る。

検証

独立レビューで見つかった中断claimの予約残留とクーポン応答順の競合を修正し、再レビューを行った。通常テスト1,337件、Firestore Emulator統合テスト13件、lint、アプリ・Functions型チェック、両ビルドが成功。390px/768pxで表示を確認。本番では購入・実注文変更・問い合わせ送信を行っていない。

商品翻訳が揃っていても購入画面に日本語が残る問題を修正

bugfixリリース済み

商品翻訳が揃っていても購入画面に日本語が残る問題を修正

リリース状況

2026年10月27日リリース済み。PR #151/#152で購入表示を修正し、本番確認で見つかったタブ名・関連案内の漏れをPR #153/#154で追補した。最終マージコミット 08677eb532e886784937f77c80d49f93952bacf7 がApp Hostingの build-2026-10-27-003 で100%配信されていることを確認した。

本番で英語・簡体字中国語のオプション説明・色・選択内容・同一カートの言語切替を確認した。カートのタブ名もそれぞれCart / 购物车となり、メール確認・404案内も選択言語で表示された。390px/768pxの表示も確認済み。確認用カートは初期の空へ戻し、表示言語・画面幅も復元した。ログイン後の購入確認・注文履歴は自動テストで確認し、本番の注文・決済は行っていない。

現行コンテンツを最後に再抽出し、翻訳対象の不足が0件であることも確認した。

何を変えたか

購入オプションのワッペン説明、直接刺繍の可否、色名、エラーと追加結果のメッセージを選択言語へ対応させる。カート・購入確認・注文履歴の商品とアイテム名にも現在の翻訳を使う。保存済みカートに古い日本語名があっても、対応するアイテムを取得できる場合は現在の翻訳から表示する。

なぜ変えたか

商品や説明の翻訳が入力されていても、最近追加した購入UIに日本語の直接記述や、日本語を既定値とするヘルパーが残っていた。商品の翻訳データ不足と、画面が翻訳を使っていない問題を分けて調べる必要があった。

ユーザーにとって何が良くなったか

英語・中国語で商品を読んだ後も、オプションの意味、選択中の構成、カートと購入確認を同じ言語で理解しやすくなる。

技術的な内容

next-intlの現在のlocaleを表示ヘルパーへ渡し、モジュール初期化時に日本語で固定されていたメッセージをフック内で選ぶ。色名は表示専用の辞書で変換し、在庫照合・カート・注文には元の日本語値を渡す。未知の自由入力色や未入力の翻訳は原文を保つ。

現行の商品名・説明・画像説明を点検した結果、公開商品の既存翻訳は入力済みだった。今回必要な不足補完は非公開の仮アイテム名だけで、原文ハッシュを検証する既存手順を利用する。新しい翻訳監査機能や自動翻訳は追加しない。

購入・ログイン関連ページのメタデータ、メール確認・404案内もリクエストの言語から生成し、noindexを維持する。

Before / After

  • Before: 商品は英語でも、ワッペン説明や色名、カートの商品構成は日本語のままになる。
  • After: 表示用ラベルと保存・照合用の値を分け、表示だけを選択言語へ変換する。

判断したこと・トレードオフ

自動化を増やす代わりに、ユーザーから点検を依頼されたタイミングで既存データと表示処理を確認する。自由入力の新しい色名は辞書にない限り原文を表示するため、今後の点検でも翻訳対象を確認する必要がある。

ハマったこと・学んだこと

翻訳の空欄がゼロでも多言語対応が完了しているとは限らない。保存済みカートのスナップショット名や、ロケールを受け取らない純粋関数も追う必要がある。

独立レビューでは、新規importがuse clientより前に入ってクライアント境界を無効にする問題を検出した。lintと型チェックの成功だけでは検出できず、ディレクティブを先頭へ戻してNext.js本番ビルドを通した。

GitHub Actions CIを専用セルフホステッドランナーへ移行

ciリリース済み

GitHub Actions CIを専用セルフホステッドランナーへ移行

リリース状況

リリース済み(2026-10-27)。masterのCI移行はPR #144、stagingのCI移行はPR #145、masterのテストタイムアウト対策はPR #146 でそれぞれマージ済み。両ブランチのPR CheckでLint、型チェック、テストの成功を確認した。stagingのpushで発火する eBay 専用CIでも主要な検証ステップの成功を確認した。

何を変えたか

PR検証用の .github/workflows/pr-check.yml と、stagingのeBay関連検証用の .github/workflows/staging-ebay-check.yml の実行先を、GitHubホスト型Linuxランナー(ubuntu-latest)から、当該リポジトリ専用のLinux/x64セルフホステッドランナーへ変更した。

# Before
runs-on: ubuntu-latest

# After
runs-on: [self-hosted, Linux, X64]

Node.js 24、actions/checkout@v4、actions/setup-node@v4、npmキャッシュ、npm ci、Lint、型チェック、テストの手順は維持した。アプリケーションコードやデプロイ構成は変更していない。

なぜ変えたか

CIの実行環境を自前で運用し、ホスト型ランナーへの依存や実行時間に伴う利用枠を抑える選択肢を持つため。すでに運用しているLinuxのsystemd管理下にリポジトリ専用のランナーを追加する方式を採用した。

ユーザーにとって何が良くなったか

直接のUI変更はない。PRごとの静的解析・自動テストを継続しつつ、CIの実行先を運用管理できるようになる。可用性や処理速度が向上するとは限らず、ホストの性能・ネットワーク・稼働状況に依存する。

技術的な内容

  • GitHub Actions Runner 2.338.0 をLinux/x64で登録し、systemdサービスとして常駐させた。
  • ランナーを対象リポジトリ専用に登録したため、標準ラベル self-hosted、Linux、X64 で絞り込める。独自ラベルの設定は不要。
  • systemdサービスは enabled かつ active (running)、GitHubとの接続と Listening for Jobs を確認した。
  • 変更対象となるworkflowファイルがmasterとstagingの両ブランチに存在するため、ブランチ別に変更PRを作成した。
  • stagingの初回PR Checkで、Functions全体を execFileSync でコンパイルするテストの beforeAll が既定の10秒を超過。該当hookだけ 60_000 ミリ秒へ拡張し、アサーションや全体のテストタイムアウトには触れなかった。
  • 1台のランナーで複数のCIジョブは直列待機になる。PRのconcurrency設定は既存のままとした。

Before / After

Before

PR Check と Staging eBay Check は ubuntu-latest 上で動作し、各実行でGitHubホスト型の実行環境を利用していた。

After

両workflowがリポジトリ専用のセルフホステッドLinux/x64ランナーを利用する。ジョブ実行のためにsystemdサービスを常駐させ、自動再起動を可能にした。

判断したこと・トレードオフ

  • リポジトリ専用登録: 共有ランナーより割り当て対象が明確。追加のラベル管理も不要。
  • CI処理を変更しない: 環境移行とテストの増減を分離し、問題発生時の原因切り分けを容易にした。
  • 分岐するworkflow定義を個別に更新: master の更新のみでは staging ブランチで実行されるworkflowに反映されないため、stagingにも明示的に適用する。
  • 運用責任: OS・ランナーの更新、依存パッケージ、容量、ログ、ネットワーク、ホストの電源管理を自前で行う必要がある。
  • セキュリティ: ワークフローから任意のコードがホスト上で動くため、信頼できないPRを実行させず、実行ユーザー権限を最小に保ち、シークレットや他リポジトリのワークスペースを分離する必要がある。

ハマったこと・学んだこと

  • セルフホステッドランナーを登録しただけではCIが移行しない。runs-on のラベルと、登録先のリポジトリの一致が必要。
  • 登録時にカスタムラベルを入力しなくても、リポジトリ専用の標準ラベルで実行先を特定できる。
  • ランナーを展開したディレクトリとサービスの svc.sh 実行ディレクトリを一致させる必要がある。
  • 複数ブランチに独立したworkflowがある場合、片方だけ直しても別ブランチのCIは旧環境で動き続ける。
  • Functionsのコンパイルを同期的に実行するテストで、stagingのPR Checkは180テストファイル中179ファイル成功したものの functions/__tests__/index-export-contract.test.ts の準備hookで Hook timed out in 10000ms となった。環境負荷に左右される処理を標準の10秒で制限すると、テスト本体に問題がなくてもCIが不安定になる。
  • 対策は beforeAll(() => { ... }, 60_000) とし、コンパイル処理にだけ猶予を与える。全体の時間制限を緩めて別の回帰を見逃すのは避ける。
  • actions/setup-node は指定したNode.jsをジョブへ提供するため、ランナーの配置時点でNode.js 24をシステム全体へインストールする必要はない。ただし各種system依存パッケージは別途確認する。

Instagramストーリーズの定期投稿をブラウザ操作から公式Graph APIへ移行

feature開発中(未リリース)

Instagramストーリーズの定期投稿をブラウザ操作から公式Graph APIへ移行

リリース状況

未リリース(開発ブランチのドラフトPR段階)。Graph API用の投稿処理、19時JSTスケジュール、画像選択、単体テスト、運用ドキュメントを追加した。Metaの認証情報、公開画像URL、実アカウントへの投稿成功は未確認で、本番自動投稿はまだ有効化していない。

何を変えたか

既存のSNS自動化ではPlaywrightでInstagramの画面を操作してストーリーズ投稿していた。これをMeta公式Graph APIの2段階公開フローに置き換える独立実行コマンドを新設し、公開時刻を毎日19:00(JST)に指定した。既存のX投稿と自動フォローは定期実行・公開npmスクリプトの経路から除外した。旧ソースコードの物理削除は残作業。

なぜ変えたか

Web画面のセレクター変更やログイン状態に依存するブラウザ自動化では、意図しない操作や投稿失敗が起きやすい。必要な機能をブランドが用意する画像のストーリーズ投稿だけに絞り、公式APIで運用を単純化するため。

ユーザーにとって何が良くなったか

画像URLのリストを用意すれば、毎日違う画像を巡回して指定時刻に投稿できる設計。投稿用ブラウザを常時開いておく必要がなく、実行履歴とDiscord通知で公開成否を確認できる。

技術的な内容

  • TypeScriptとNode.jsのFetch APIでMeta Graph API v26.0へリクエスト。
  • POST /{ig-user-id}/media に media_type=STORIES と公開HTTPS画像URLを渡してメディアコンテナを作成。
  • GET /{container-id}?fields=status_code の FINISHED を待機してから POST /{ig-user-id}/media_publish で公開。
  • タイムゾーンAsia/Tokyoから当日の日付を計算し、日付と登録枚数による剰余で画像を巡回。実行を繰り返しても同じ日の画像選択が一定になる。
  • GitHub Actionsのcron 0 10 * * * はUTC10時=JST19時。GitHub側で遅延する可能性がある。
  • SecretsにアカウントIDとアクセストークンを保管し、IG_STORY_AUTOMATION_ENABLEDで初期状態を無効にする。
  • ドライランモード、単体テスト、投稿結果の記録と任意のDiscord通知を追加。

Before / After

Before

Playwrightでログイン済みブラウザを立ち上げ、InstagramとXへ画面操作で投稿し、自動フォロー処理も同じ定期実行経路に含まれた。

After

Instagramストーリーズ用のGraph API処理のみを定期実行するように変更。X・フォローの旧実装コードはレガシーとして残るが、新しいスケジュールと公開コマンドからは実行しない。

判断したこと・トレードオフ

  • 投稿画像の指定には公開HTTPS URLが必要。プライベートGitHubの画像ファイルをそのままAPIへ渡す方式は採用できない。
  • GitHub Actionsなら常駐サーバー不要。ただし19時ちょうどの起動保証はなく、手動再実行で重複投稿する可能性がある。
  • Metaの認証情報・公開画像がそろうまでは、本番で実行しないフェイルクローズ構成にした。
  • 画像上の文字・リンクスタンプ・アンケートなどはAPI投稿処理で追加しない。

ハマったこと・学んだこと

  • Instagramストーリーズは通常の画像投稿と異なり、media_type=STORIES を指定する必要がある。
  • メディアコンテナの作成成功は公開完了ではない。コンテナ処理完了を確認してからmedia_publishする。
  • UTC基準のcronをそのまま19時に設定すると日本時間4時になってしまう。JSTとの差9時間を補正する。
  • トークン・画像URLが未設定なのに定期実行を有効にすると、初日から失敗するため安全な初期無効が必要。
  • 日付で画像を決定する設計はステートレスだが、同日中の複数実行に対する二重投稿防止ではない。

Instagram商品カタログの商品名をGoogle掲載と統一

improvementリリース済み

Instagram商品カタログの商品名をGoogle掲載と統一

リリース状況

リリース済み。2026年10月14日に独立コードレビューとCIを経てstaging、masterへマージし、本番のMeta通常同期workerと日次照合関数を更新した。配布ソースとの一致を確認し、通常のカタログ同期を1回開始した。画面・商品タグの手動動作確認はユーザーが担当し、こちらでは実施していない。商品情報の表示更新はMeta側の処理で順次反映される。

何を変えたか

Instagram用の商品カタログの商品名を、Google向けと同じアイテム名・刺繍柄名・色・サイズの形式へ統一する。商品タグの参照先を維持するため、既存の商品IDと商品グループIDは変更しない。

なぜ変えたか

Googleの商品名を改善した際、Meta側は独自の商品名生成を維持していた。Instagramでも購入対象のアイテムと組み合わせが分かりやすい商品名に揃えるため。

ユーザーにとって何が良くなったか

Instagramの商品タグから開く商品情報でも、アイテムの種類・柄・色・サイズを把握しやすくする。投稿のキャプションは編集しない。

技術的な内容

Metaの商品投影で既存のMerchant商品投影を利用し、商品名も共通化する。同じretailer IDを用いたUPDATEで商品を更新し、削除・再作成によって既存タグの参照先が変わることを避ける。

独立レビューでは、日次照合もカタログを直接更新する点を確認した。通常workerのみ更新して古い日次処理が商品名を戻すことを防ぐため、両関数を同じコードへ更新する。

判断したこと・トレードオフ

商品タグの表示情報と、投稿本文へ手入力した文章は別物として扱う。IDを変えずに商品名だけを更新することを回帰テストで確認する。本番の画面・商品タグの手動動作確認はユーザーの希望によりユーザーが担当する。

ハマったこと・学んだこと

共通の商品投影を呼び出していても、連携先の処理で商品名を上書きすると、共通処理の改善が一部のチャネルにしか反映されない。チャネルごとの最終ペイロードを検証する必要がある。外部の商品IDを固定して、既存の参照関係を維持することも重要。通常workerに加えて日次照合も直接カタログを更新する構成では、両方の関数を同じコードへ更新しないと、日次処理が古い名称へ戻す可能性がある。

商品の初期HTMLと3言語の検索向け情報を整える

improvementリリース済み

商品の初期HTMLと3言語の検索向け情報を整える

リリース状況

2026年10月14日リリース済み。PR #139を検証環境へ、PR #140を本番へ通常マージした。App Hostingで本番マージコミット ef6bcf1d8c47aa5ced0553c2c4415b5480a0ed80 の100%配信と、関連Functions 6件の更新を独立確認した。翻訳40フィールド・19ドキュメントの更新と公開HTMLも確認済み。

何を変えたか

商品ページの初期選択、表示価格・在庫、検索エンジン向けの商品情報を同じデータから生成する。日本語の既存URLを維持し、英語と簡体字中国語の公開URLを追加する。商品とキャラクターを結ぶ通常の内部リンクも整える。

日本語の商品説明を原文とし、別商品を指していた翻訳や古い紹介文、未翻訳の項目を英語・中国語で揃える。日本語の文体は維持し、確認済みの商品名の取り違えだけを修正する。

なぜ変えたか

Search Consoleで未登録URLが多かったため、理由別の対象URL、現在の公開HTML、サイトマップ、クロール統計を照合した。未登録には、購入・認証画面の除外やHTTP版からHTTPS版への転送など、適切な状態も含まれていた。

一方、公開商品ページの初期HTMLでは売り切れと表示しながら、構造化データは在庫ありを示す不一致があった。韓国向けURLでは本文がウォン、構造化データが円になっていた。同じURLをCookieだけで別言語へ切り替える構成では、言語ごとのページを検索エンジンへ明示しにくかった。

ユーザーにとって何が良くなるか

  • 商品を開いた時点から、実在する在庫ありの組み合わせと価格が表示される。
  • 広告や共有リンクで指定したアイテム・色・サイズを維持できる。
  • 英語・中国語のページをその言語のURLで共有し、同じ言語のままページを移動できる。
  • キャラクターの紹介から対応商品へ、商品からキャラクターやアイテムへ移動しやすくなる。
  • 日本語と外国語で、別商品の名前や説明を表示してしまう状態を解消する。

技術的な内容

初期選択とOfferを共通化する

ブラウザーのEffectでサイズや色を選ぶだけでは、サーバーが出力したHTMLには選択前の在庫が残る。選択処理を純粋関数へ切り出し、サーバー描画とクライアント初期状態が同じ構成を参照するようにする。

在庫あり商品の自動選択と、リンクで明示された売り切れ商品の選択は分ける。売り切れの指定を別商品へ黙って変更すると、広告や共有リンクの意味が変わるためである。購入実行については引き続きライブデータの準備完了を条件にする。

言語と販売先を分ける

日本語は既存URL、英語は /en/...、簡体字中国語は /zh-CN/... を使う。公開ページの言語はURLで固定し、販売先や通貨の指定と分離する。管理・API・認証・購入処理へ公開ページ用の書き換えを適用しない。

各翻訳ページは自身を正規URLにし、対応言語間をhreflangで結ぶ。主要本文が未翻訳のページを、翻訳済みの独立した検索対象として扱わない。サイトマップにも検索対象の正規ページだけを掲載する。

翻訳の欠落と取り違えを別々に確認する

翻訳フィールドが埋まっていても、別の商品説明が入っている場合がある。欠落件数だけで完了とせず、日本語原文、既存訳、変更予定の訳を比較する。原文のハッシュや更新前の値を照合し、並行編集を上書きしないようにする。

Before / After

Before

初期HTMLの売り切れ表示と構造化データの在庫あり表示が一致せず、国別URLの通貨も揃っていなかった。言語切替は主にCookieとブラウザーの言語設定へ依存していた。

After(本番で確認済み)

同じ構成・販売先・通貨に対し、初期HTMLと商品情報を一致させた。言語別のURL、本文、正規URL、言語間リンク、サイトマップを揃えた。全1,228テスト、lint・型チェック・production buildとCIが成功。独立担当が本番HTTP44項目、代表商品の3言語×2通貨の12項目、サイトマップ174URL全件を検証し、失敗は0だった。

サイトマップは日本語・英語・簡体字中国語それぞれ58URL。全掲載URLでHTTP 200、自身を指すcanonical、インデックス可能なrobots、期待言語のHTMLを確認した。翻訳前後の60ドキュメント・281フィールドを照合し、未翻訳25項目は0になった。無関係な翻訳の変更はなく、日本語原文の変更は指定されたSKATER説明の誤記修正だけだった。

判断したこと・トレードオフ

  • 日本語の既存URLを残し、すべてのページを新しい /ja URLへ移す変更を避ける。
  • 日本語の商品説明の雰囲気をSEO目的だけで書き換えず、外国語訳を現在の原文へ揃える。
  • Googleの登録件数を直接の完了条件とせず、サイト側で検証できるHTTP応答、本文、内部リンク、検索向け情報を条件にする。

ハマったこと・学んだこと

Search Consoleの集計と個別URLの最新状態は更新タイミングが異なる。「検出のみ」の一覧に残ったURLが、後日のクロール履歴には存在することがあった。ソフト404の記録があるページでも、公開URLテストでは登録可能と判定された。古い集計の件数だけから、現在も同じ障害が起きていると結論づけないことが重要だった。

また、構造化データの構文が有効でも、画面と在庫・通貨・商品構成が一致しているとは限らない。画面とJSON-LDの両方を同じ構成で検証する必要がある。

言語別URLは実ルートとして用意した。当初の内部rewriteではproxyが内部URLでも動き、言語が日本語へ戻る問題をローカルHTTP確認で発見した。言語の判定をURLへ揃え、ルートごとの出力を確認した。

独立レビューでは、Markdown内の自サイトリンク、翻訳不足の案内、重複クエリの解釈も確認した。root layoutだけでページ固有の案内を計算するとクライアント遷移時に更新されないため、案内は各ページで再評価する。サーバーのクエリ配列とブラウザーのURLSearchParamsは、同じ先頭値を採用してから商品構成を検証する。重複したitemパラメーターでも本文価格とOfferが一致する回帰確認を加えた。

Google商品掲載の商品情報とバリエーションリンクを改善

improvementリリース済み

Google商品掲載の商品情報とバリエーションリンクを改善

リリース状況

リリース済み。2026年10月13日に独立レビューとCIを経てstaging、masterへ順にマージし、本番の関連同期ワーカーを更新した。稼働中ソースとの一致、代表商品のGoogle側の商品情報・日本の無料掲載承認、商品リンクの初期選択を確認した。全商品・海外向けの情報は既存の同期処理で順次更新される。検索結果でのクリック増加はまだ評価していない。

何を変えたか

Google Merchant Centerへ送る商品情報を、刺繍柄だけでなく実際に購入する本体アイテムと結び付ける。商品名にアイテム名を加え、説明に登録済みのアイテム説明を含め、リンクにアイテム・本体カラー・サイズを指定する。

なぜ変えたか

同じ刺繍柄を複数の本体アイテムへ組み合わせる構成で、従来の商品名は柄名・色・サイズが中心だった。購入するアイテムの種類や特徴が伝わりにくく、遷移先も掲載された組み合わせを初期選択できていなかったため。

ユーザーにとって何が良くなったか

検索結果の商品情報からアイテムの特徴を把握しやすくなり、掲載されたアイテム・色・サイズを引き継いで商品ページを開けるようにする。

技術的な内容

Firestoreの商品と共通アイテムマスターからMerchant APIの商品属性を生成する。登録済みの多言語情報を利用し、未翻訳のアイテム説明を英語・中国語のフィードへ混ぜない。既存のoffer IDを維持し、商品属性の変更として同期する。既存の商品詳細画面が対応しているクエリーパラメーターを利用する。説明文のMarkdown装飾を除去し、タイトルは150文字、説明は5000文字以内に整える。既存の刺繍合成プレビュー画像の優先順位を維持する。

Before / After

Before

柄名・色・サイズだけの商品名。柄の説明のみ。商品詳細URLにアイテムの指定がない。

After

実際のアイテム名と柄名を含む商品名。登録済みの柄とアイテムの説明を掲載。URLから掲載された組み合わせを初期選択する。

判断したこと・トレードオフ

  • 生地の厚さ・素材・機能を推測して付け加えず、登録された実データを使う。
  • 色を保証できない画像を勝手に主画像へ変更しない。
  • 画像の表示対象が別アイテムのものは、画像候補から除外する。
  • 集客効果はデプロイ成功と分けて評価し、一定期間の表示回数・クリック率で測定する。

ハマったこと・学んだこと

商品マスターを分離したECでは、柄の名前だけでフィードを作ると購入対象を十分に表現できない。商品属性だけでなく、リンク先の初期選択、言語、画像の適用対象を同じバリエーションへ揃える必要がある。Merchant APIでの受理と検索結果での掲載・流入は、それぞれ別に確認する。

Googleの商品画像同期を時間切れ前に保存して続行する

fixリリース済み

Googleの商品画像同期を時間切れ前に保存して続行する

何を変えたか

日次照合で同じMerchantアカウントを4配信先ごとに読み直していた処理を、アカウントごとに1回の全件取得へ変更した。取得を1000件/ページにまとめ、取得責務を独立モジュールへ分けた。全ページ成功後に同期状態の照合とキュー登録を行う。

大量商品同期は、次のAPI呼び出し前に6分の処理予算を確認する。時間を使った場合は更新済みofferのhashを維持してpendingへ戻し、次回は未更新の入力から再開する。リースを解除し、失敗回数を増やさない。404から再登録、409から更新、削除の各呼び出し境界にも適用する。

なぜ変えたか

刺繍合成画像への大量切り替えでは、日次照合と同期workerの両方が540秒の実行上限へ達していた。送信が成功していてもworkerの終了がエラーになるため、正常な継続に変える必要があった。

ユーザーにとって何が良くなったか

大量の商品画像更新を保存済みデータから続けられ、一時的な遅さや初回大量更新によって失敗回数を消費しにくくなった。

検証

独立レビュー指摘ゼロ。Functions 36ファイル360テスト、lint、アプリとFunctionsの型チェック、両ビルドに成功。本番の全4配信先を新helperで35通信・約86秒で読み取れた。本番の日次照合は約81秒、大量同期workerは約370秒でHTTP 200の正常終了を確認した。worker終了時点で16件はpendingへ戻り、エラージョブは0件だった。Metaの全11,168件は画像URL一致、欠損・差分0を最終確認した。Googleの外部画像一致は全4配信先で16,214件まで増えていた。後のチェックポイントでは20,112件の送信済み状態を確認した。取得時点が異なり、送信済み件数と外部反映件数も異なるため、全件画像更新の完了は断定しない。

リリース状況

stagingからmasterへマージし、関係するGoogle同期Functionsへ本番反映した。停止していた定期実行を再開した。

判断と制約

6分の予算は次のAPI開始を止めるためのもの。単一APIの最大約93.5秒の再試行と状態保存に余裕を残す。データベースや認証の通信そのものを強制終了するdeadlineではない。通常のAPIエラーでは従来の失敗予算を使い、時間による区切りだけを継続と扱う。

同じ配信先名が複数にある場合の上書き順やdisabled除外を維持した。全ページ取得に失敗すると照合を開始しない。アカウント一覧の重複取得と大量書き込みの終了条件は別々に直す必要があった。

学び

APIが成功するかだけでなく、スケジューラが正常終了し、更新を次回へ引き継げるかまで確かめる。大量更新は有限な処理予算・チェックポイント・再実行の組み合わせで扱う。

Googleの商品画像同期を一時的な通信エラーから復帰させる

fixリリース済み

Googleの商品画像同期を一時的な通信エラーから復帰させる

何を変えたか

Merchant APIの単一リクエストについて、HTTP 5xx・TimeoutError・明示的な通信エラーだけを最大3試行する。1秒・2秒の指数バックオフに0〜250msのjitterを加える。各通信の30秒期限を維持し、3回失敗した場合は既存のジョブ単位再試行へ戻す。

なぜ変えたか

刺繍合成画像への大量切り替え中、Googleの一時的な500で商品全体の同期を中断し、ジョブの再試行予算を消費していたため。正常な登録でも実API応答に約22秒かかるケースがあった。

ユーザーにとって何が良くなったか

一時的な通信エラーの直後に同じ入力で復帰できるようにし、商品の画像更新を進めやすくした。

判断したこと・トレードオフ

認証・入力不正・404・409・429は短期再試行の対象から除外する。404の商品再登録と409の更新への切り替え、レート制限のジョブ待機を既存の担当処理へ返す。有限再試行のため、外部API障害そのものを解消する仕組みではない。

判定と検証

独立レビュー指摘ゼロ。Functions 35ファイル345テスト、lint、アプリとFunctionsの型チェック、Functionsビルドが成功。実APIで単一商品の更新200、新規登録200、不存在404を確認した。今回の実通信では500から200への復帰は再現しなかったため、その分岐は回帰テストで検証した。

Metaは全商品の画像一致を確認済み。Googleは画像一致が前回の4,341件から9,826件へ増えたが、未登録・旧画像は残っている。全件照合は34ページを読み切って成功した。外部反映が全件完了したとは扱わない。

リリース状況

stagingからmasterへマージし、Google同期の関連Functionsへ反映済み。

学び

ジョブの再試行と、単一通信の短期再試行は目的を分ける。APIの正常応答時間・送信結果・外部に登録された画像を別々に検証する。保存済みoffer hashがあれば、処理時間の上限で中断しても更新済み入力から再開できる。ただし、全ページ照合そのものの540秒制約は別の運用上の課題として残る。

商品カタログ同期の登録欠落・送信制限・遅い応答から回復

fixリリース済み

商品カタログ同期の登録欠落・送信制限・遅い応答から回復

何を変えたか

Googleの商品入力の更新が404となった場合、同じoffer IDを再登録する。再登録の409は一度だけ更新へ戻し、失敗時は送信hashを更新しない。データソースそのものは作成しない。

Metaのカタログ送信制限80014を再試行可能として扱う。workerと日次照合の両方で残商品への送信を止め、Retry-AfterとBUCヘッダーの待機時間を反映してキューに残す。制限待ちは通常エラーの再試行予算を消費しない。

Google APIの期限を5秒から30秒に延ばし、TimeoutErrorを明示的に再試行へ渡す。

なぜ変えたか

刺繍画像への切り替え後の実データ確認で、Google15商品の更新がNOT_FOUNDで停止した。アプリには過去の送信hashが残っていたが、Googleには該当商品入力が存在せず、実APIのエラー詳細はITEM_NOT_FOUNDを示した。

Metaでは短時間の大量送信で6商品が80014となった。制限解除後に同じ商品のbatch送信が成功し、分単位のカタログ制限であることをAPIの使用率ヘッダーで確認した。

Googleの正常応答にも5〜9秒かかるケースを実測した。5秒の期限では成功できるリクエストまで中断していた。

ユーザーにとって何が良くなったか

外部側で消えた商品を復元でき、一時的な送信制限や遅い応答によって商品画像の同期が永久停止しにくくなる。画像生成が正常でも送信が失敗する経路を対象にした改善。

判断と検証

  • 永久的な権限・入力不備と、一時的な制限を区別する。
  • 新しいsource-writeや別workerの処理を延期処理で上書きしない。
  • APIの期限を無制限にせず30秒に限定する。
  • 独立レビューの指摘を解消し、関連339テスト、lint、アプリ/Functions型チェック、GitHub CIで確認した。
  • Metaは公開16商品全11,168件について、実カタログの画像URLと期待値の一致を確認した。
  • Googleは再登録・更新のHTTP200を実APIで確認し、既存商品の再同期を進めている。全件の外部反映完了とは分けて扱う。

リリース状況

stagingとmasterへマージし、関連Functionsへ配信済み。Googleの画像更新は成功記録を保存しながら順次進む。

学び

画像APIの200やテスト通過だけでは、外部カタログへの配信完了は証明できない。送信結果、外部の実商品、画像URLをそれぞれ確認する必要がある。レート制限の延期を通常エラー回数へ加えると、待機後の最初の通信失敗が永久エラーになるため、再試行予算の意味を分ける。

Instagram・Googleの商品画像に刺繍プレビューを反映

fixリリース済み

Instagram・Googleの商品画像に刺繍プレビューを反映

何を変えたか

外部商品カタログへ送る画像を、無地の本体写真から、登録済みのキャラクターイラストを重ねた商品画像へ変更した。本体カラーごとに配置・大きさ・回転と刺繍色を反映したPNGを生成する。設定が変わると画像URLも変わる。

なぜ変えたか

Instagramのショップに、SPRAY MAXなどのキャラクターが描かれていないTシャツ写真が表示されていた。商品ページのCSSによるリアルタイムプレビューはブラウザ内の表示であり、外部サービスが取得する画像ファイルには反映されていなかった。

ユーザーにとって何が良くなったか

商品一覧から、どのイラストを使った刺繍商品か判断しやすくなる。登録済み刺繍色の中から本体とコントラストが高い代表色を選ぶため、白いTシャツに白いイラストを載せて見えにくくするケースも避ける。

技術的な内容

Next.js Route Handlerで公開商品・公開本体・紐付けと本体色を確認し、Sharpで1200px正方形PNGを生成する。CSSと同じobject-contain、alpha mask、中心基準の配置・回転を使用する。刺繍デザインの優先順位と空欄の扱いを商品ページに合わせ、スウォッチ解決はUIとFunctionsで共有した。

MetaとGoogle Merchantの投影処理で共通の合成画像URLを使う。画像の素材・配置・刺繍色・本体スウォッチ・更新情報からバージョンを作り、外部サービスの画像再取得を促す。デザイン付き商品で必要な画像が欠けた場合は無地画像へ戻さず、同期エラーとして扱う。

Before / After

Before

外部カタログは本体カラー写真を優先し、ブラウザ内だけに存在する刺繍レイヤーが欠落していた。Googleの同期は意図的に非公開にした紐付け本体も不整合として扱い、商品の公開本体の更新まで停止していた。

After

イラストを含むPNGを送信する。Google同期も、非公開本体を対象から外して公開offerを更新し、過去の非公開offerを削除する。参照欠落や不正な在庫による保全は維持する。

判断したこと・トレードオフ

  • 外部カタログの代表色は登録済み色の中で安定して選び、商品ページのランダムな初期刺繍色に依存させない。
  • 未知の色名を固定色で埋めず、既知のスウォッチがない場合は登録順を保持する。
  • APIは取得可能な画像ホストを制限し、リダイレクト・過大画像を拒否する。
  • Web APIを配信してから関連Functionsを更新し、未配信URLを先に外部サービスへ送らない。

ハマったこと・学んだこと

  • DOMの見た目と、外部サービスが取得する画像バイナリは別物。CSSマスクの合成にはサーバー側の画像生成が必要だった。
  • アルファチャンネルを結合した直後のSharp回転で色が黒くなるケースをピクセルテストで検出した。着色マスクを一度PNGにしてから回転する2段階処理にした。
  • ||と??による空文字の扱いの違いで、商品ページと別のデザインが選ばれる不一致を独立レビューで検出した。
  • 「非公開」は意図された同期除外であり、参照欠落と同じエラーとして扱うと公開商品の更新まで停止する。

React 19 + Vitestでテスト終了後にwindow is not definedが出るケースを分離して直した

ciリリース済み

React 19 + Vitestでテスト終了後にwindow is not definedが出るケースを分離して直した

何が起きたか

PR #116 のCIで、個々のテスト自体はすべて成功しているにもかかわらず、Vitestの最後に次のUnhandled Errorが発生してジョブ全体が失敗した。

ReferenceError: window is not defined

Vitest caught 2 unhandled errors during the test run.
Test Files 167 passed
Tests 1110 passed
Errors 2 errors

エラー元として app/character/[id]/page.test.tsx が示されていた。

原因

CharacterDetailPage のページ単体テストで、クライアントコンポーネント ImageWithFallback まで実際に描画していた。

ImageWithFallback は useEffect とstate更新を持つため、React 19のスケジューラ上で非同期処理がテスト終了後まで残ることがある。

テスト環境のjsdomが破棄された後にReact側の処理が走ると、React DOM内部が window を参照して ReferenceError: window is not defined になる。

今回のテストが確認したかったのはキャラクター詳細ページのSSR出力・翻訳・背景クラス・canonical redirectであり、画像フォールバックコンポーネント自体の動作ではなかった。

修正

app/character/[id]/page.test.tsx で ImageWithFallback を副作用のないmockへ置き換えた。

vi.mock("@/components/ui/ImageWithFallback", () => ({
  default: ({ alt }: { alt?: string }) => (
    <span role="img" aria-label={alt ?? ""} />
  )
}));

背景色クラスの検証は、mockした role="img" の親要素に対して継続した。

ImageWithFallback 固有のDOM属性やfallback挙動は既存の ImageWithFallback.test.tsx に任せ、ページ単体テストではページ責務だけを検証する構成にした。

なぜ単純に待機を追加しなかったか

waitFor や任意のdelayで非同期処理が終わるのを待つ方法も考えられるが、テスト対象外の子コンポーネントの内部実装へページテストが依存する状態は残る。

ページ単体テストでは依存コンポーネントを適切にmockし、各コンポーネントの責務をそれぞれのテストへ分離した方が安定する。

検証

修正後のPR Check #109で次がすべて成功した。

  • Lint app
  • Typecheck app
  • Typecheck Functions
  • Run tests

全テスト成功後もUnhandled Errorは発生していない。

ハマりどころ

  • Vitestでは「テスト1110件すべてpassed」でもUnhandled Errorが残っていれば終了コード1になる。
  • エラー表示に特定のテストファイル名が出ても、assertion失敗とは限らず、Reactの非同期処理がテスト終了後に残っている場合がある。
  • Reactクライアントコンポーネントをサーバーページの単体テストへそのまま含めると、不要な副作用までテストへ持ち込むことがある。
  • テスト対象の責務外にある子コンポーネントはmockし、その子の挙動は専用テストで確認するとCIが安定しやすい。

追加対応: client scheduler自体を起動しない構成へ変更

PR #116で子コンポーネントをmockして一度安定化した後、staging→masterの統合CIで同じUnhandled Errorが再発した。

そこでPR #118では、Server Componentである CharacterDetailPage のテストに @testing-library/react のclient render() を使うこと自体をやめ、react-dom/server の renderToStaticMarkup() でSSR結果を文字列化して検証する構成へ変更した。

これによりReact DOM client schedulerを起動しないため、jsdom破棄後に残った Immediate が window を参照する経路そのものをなくした。名前・プロフィール・背景クラス・canonical redirectの検証意図は維持している。

PR #118のPR Check #117、さらにstaging→masterのPR #117で実行されたPR Check #118の両方で、lint / App typecheck / Functions typecheck / 全テストが成功した。

リリース状況

リリース済み。

  • PR #118: stagingへマージ済み(2026-10-02)
  • PR #117: staging→masterへマージ済み(2026-10-02)
  • 本番反映対象のmasterへ取り込み完了

Metaカタログのイベント未送信と0%マッチレートを修正

bugfixリリース済み

Metaカタログのイベント未送信と0%マッチレートを修正

何を変えたか

Meta Commerce Managerで、カタログマッチレートが0%になり、過去7日間の ViewContent、AddToCart、Purchase イベントを受信していない状態を調査した。

商品カタログへの同期処理自体は存在していたが、ストアフロント側ではGA4向けe-commerceイベントしか送信しておらず、Meta Pixelの標準イベントを送信する実装がなかった。

PR #115で次を追加した。

  • NEXT_PUBLIC_META_PIXEL_ID が設定されている場合だけMeta Pixelを初期化
  • 商品詳細表示で ViewContent を送信
  • カート追加成功時に AddToCart を送信
  • チェックアウト開始時に InitiateCheckout を送信
  • Stripe決済確認後に Purchase を送信
  • Metaカタログの商品IDと完全一致する content_ids / contents[].id を生成
  • 商品バリアントを一意に特定できない場合は、不一致イベントを送らずスキップ
  • ID生成とイベントpayloadの回帰テストを追加

PR Checkではlint、App TypeScript typecheck、Functions typecheck、testsがすべて成功し、stagingへマージした。

原因

Metaの商品カタログでは、Productだけではなくlinked Item、色、サイズを組み合わせた販売可能バリアントごとに商品IDを生成している。

既存のカタログ同期では次の入力からSHA-256ベースの安定IDを作る。

productId
itemId
itemColor
size
  ↓
JSON.stringify([...])
  ↓
SHA-256
  ↓
base64url
  ↓
mtw-<hash>

一方、Webサイト側にはMeta Pixel自体の初期化と商品イベント送信が存在しなかった。

そのためCommerce Managerから見ると、カタログには商品が存在していても、Web上の商品閲覧・カート追加・購入をカタログ商品へ結び付けるイベントが1件も届かない状態だった。

なぜ商品IDを単純なproductIdにしなかったか

Metaカタログ側の正本IDはすでに色・サイズ単位のバリアントIDになっている。

Pixel側だけ productId を送ると、イベント自体は受信されてもカタログ内の商品IDと一致せず、マッチレートが改善しない。

そこでPixel側でもカタログ同期と同じ入力・同じSHA-256・同じbase64url形式を使い、完全に同じIDを再現する設計にした。

Catalog item id
mtw-xxxxxxxx

Pixel content_ids
["mtw-xxxxxxxx"]

この2つが同じ文字列になることが重要。

Purchaseを注文作成時ではなく決済確認後に送る

Purchase は注文レコードを作成したタイミングでは送信しない。

このECでは注文作成後にStripe Checkoutへ遷移し、戻り先で confirmStripeCheckoutOrderPayment によって支払い状態を確認している。

そのためMetaの Purchase も、既存GA4 purchaseと同じく支払い済みが確認された後だけ送る。

これにより「注文を作ったが決済しなかったケース」を購入として誤計測するのを防ぐ。

不完全なバリアントIDは送らない

itemId や itemColor が不足した状態で代替IDを作ると、Metaにはイベントが届いてもカタログと一致しないIDが蓄積する。

そのため、カタログ上の販売バリアントを確実に特定できない場合はMeta商品イベントを送信しない。

イベント件数を増やすことより、誤った content_ids でマッチレートを悪化させないことを優先した。

CIで見つかった問題

最初の実装ではNext.jsの next/script に strategy="beforeInteractive" を指定した。

しかしApp Router構成ではESLintの @next/next/no-before-interactive-script-outside-document 警告が発生し、このリポジトリは --max-warnings 0 のためCIが失敗した。

afterInteractive へ変更し、Next.jsで許可される読み込み方法に修正した後、CIを再実行して全チェックが成功した。

Before / After

Before

  • Metaカタログの商品同期は動作していた
  • WebサイトはGA4のe-commerceイベントだけを送信
  • Meta Pixelの商品イベントは未実装
  • Commerce ManagerではViewContent / AddToCart / Purchaseが未受信
  • カタログマッチレートは0%

After

  • Meta Pixelを環境変数で安全に有効化できる
  • 商品詳細・カート・チェックアウト・決済完了をMetaへ送信できる
  • content_ids はカタログの商品バリアントIDと同じ方式で生成する
  • バリアント情報が不足したイベントは送らない
  • Pixelの障害が購入フローを止めないよう、トラッキング失敗は本処理から分離している

判断したこと・トレードオフ

  • 画面に表示されていた「アプリID」をPixel IDとしてハードコードしなかった。MetaのApp IDとWeb Pixel / Dataset IDは用途が異なるため、正しいイベントソースIDを確認してから設定する。
  • stagingでは本番Pixelへテストイベントを混ぜないため、Pixel IDを自動で本番値へ固定しない。
  • Metaイベント送信失敗によって商品閲覧・カート追加・購入処理が失敗しない構成にした。
  • Catalog側のID形式を変更せず、Pixel側を既存Catalog Projectionへ合わせた。既存の商品同期や商品セットへの影響を避けるため。

ハマったこと・学んだこと

  • 「商品がカタログに登録されている」ことと「商品イベントがカタログへマッチする」ことは別問題。
  • Metaの商品イベントでは content_ids の値がカタログの商品IDと一致している必要がある。イベントを送るだけでは不十分。
  • Product / variantを分けているECでは、親商品IDではなく実際にカタログへ登録しているバリアントIDを送る必要がある。
  • 決済イベントはブラウザのボタン押下ではなく、サーバー側で支払い完了を確認した後に送る方が誤計測を防ぎやすい。
  • Next.jsのScript読み込み戦略はApp RouterとPages Routerで利用条件が異なり、CIのNext.js ESLintルールで検出できる。

リリース状況

リリース済み。

  • PR #115: Meta Pixelの商品イベント実装をstagingへ反映済み
  • PR #116: 本番App Hosting用の NEXT_PUBLIC_META_PIXEL_ID=28127280380307083 を設定済み
  • PR #117: staging→masterへマージ済み(2026-10-02)
  • リリースPR #117の最新PR Check #118でlint / App typecheck / Functions typecheck / 全テスト成功
  • 本番反映対象のmasterへ取り込み完了

Commerce Managerの過去7日/28日のマッチレートはローリング集計のため、本番で新しいイベントが入り始めても表示が即時に100%へ切り替わるとは限らない。

Firebase FunctionsのApp Check Paramでデプロイ時に出る誤警告への対応

bugfixリリース済み

Firebase FunctionsのApp Check Paramでデプロイ時に出る誤警告への対応

何を変えたか

client error reporting用Callable FunctionのApp Check段階導入設定を、onCall({ enforceAppCheck }) のParam指定から、リクエスト実行時の明示的な判定へ変更した。

CLIENT_ERROR_APP_CHECK_ENFORCED がtrueの場合だけ、Firebase Functions SDKが検証済みの request.app が存在することを確認し、存在しなければ unauthenticated で拒否する。

なぜ変えたか

Firebase Functionsのデプロイ中に次の警告が出ていた。

params.CLIENT_ERROR_APP_CHECK_ENFORCED.value() invoked during function deployment, instead of during runtime.
This is usually a mistake. In configs, use Params directly without calling .value().

アプリ側では .value() を呼ばず、公式のParam利用方法に沿って enforceAppCheck: clientErrorAppCheckEnforced と指定していた。

調査すると、使用中の firebase-functions 7.3.2では onCall() の初期化処理が Expression<boolean> の enforceAppCheck に対してSDK内部で .value() を呼んでいた。Firebase CLIがdeploy manifestを解析する際は FUNCTIONS_CONTROL_API=true のため、SDK内部の呼び出しでも警告が発生する。

Firebase Functions本体にも、このfalse-positive warningを解消するPR #1921が存在する。

ユーザーにとって何が良くなったか

エンドユーザー向けの現在の挙動は変わらない。App Check enforcementはproduction / stagingとも現時点ではfalseの段階導入中。

運用面では、デプロイ時に本当の設定ミスと紛らわしい警告が出なくなり、Firebase CLIのログを確認しやすくなる。

技術的な内容

変更前:

onCall({
  enforceAppCheck: clientErrorAppCheckEnforced
}, handler);

変更後はCallable Functionのハンドラ実行時にParamを解決する。

if (clientErrorAppCheckEnforced.value() && !request.app) {
  throw new HttpsError("unauthenticated", "App Check verification required");
}

request.app はCallable Functions側でApp Checkトークンの検証に成功した場合に得られる情報であり、設定値がtrueの環境ではその有無をゲートとして利用する。

さらに、ビルド済みFunctions entrypointを FUNCTIONS_CONTROL_API=true で別プロセスから読み込み、対象のdeploy warningがstderrへ出ないことを確認する回帰テストを追加した。

Before / After

Before

コードではParamを直接渡しているにもかかわらず、Firebase Functions SDK内部の .value() 呼び出しによりデプロイ時警告が出ていた。

After

Paramの値はリクエスト処理時だけ読み取るため、deploy manifest解析中に CLIENT_ERROR_APP_CHECK_ENFORCED.value() は実行されない。

判断したこと・トレードオフ

  • firebase-functions の単純なバージョンアップは採用しなかった。確認時点の7.4.0でも同じeager .value() 実装が残っていたため。
  • SDKへパッチを当てたりforkを保持したりせず、アプリ側の小さい回避策にした。
  • SDKの公式修正がリリースされた後は、標準の enforceAppCheck: Param へ戻せる可能性がある。
  • App Checkトークン自体の検証を独自実装せず、Callable SDKが設定する検証済み request.app を利用した。

ハマったこと・学んだこと

  • 警告文に特定Paramの .value() と表示されても、アプリコードが直接 .value() を呼んでいるとは限らない。
  • Firebase CLIのdeploy discoveryではFunctionモジュール自体が読み込まれるため、onCall() の初期化処理もデプロイ時に走る。
  • ライブラリ内部実装まで追うことで、「公式推奨どおりに書いているのに警告が出る」ケースを切り分けられる。
  • デプロイ時だけ発生する問題は、FUNCTIONS_CONTROL_API=true を付けてビルド済みentrypointを別プロセスでrequireするテストにすると再現性を持たせやすい。

リリース状況

リリース済み。

  • PR #114: stagingへマージ済み(2026-10-02)
  • PR #117: staging→masterへマージ済み(2026-10-02)
  • PR #114のPR Check #111、およびリリースPR #117のPR Check #118でlint / App typecheck / Functions typecheck / 全テスト成功
  • 本番反映対象のmasterへ取り込み完了

設定エラー診断のレビュー対応(未リリース)

bugfix開発中(未リリース)

設定エラー診断のレビュー対応(未リリース)

何を変えたか

設定不正の通知に明示的なserverConfirmed=trueを要求する。フロントはFirestoreの!fromCache && !hasPendingWritesを送信し、Functionsはbooleanとして検証する。旧クライアント・診断情報不足・未確定の報告は保存とログだけを行う。他の操作エラーの通知条件は維持する。

Webhookが無効でも、未確定・情報不足からサーバー確定への遷移は即時保存・ログ出力する。確定状態の繰り返しは通常の保存間隔、Discord送信は既存の重複抑制を維持する。

なぜ変えたか

fromCache !== trueだけでは、情報が欠落した報告も送信できてしまう。また通知可否を即時保存の条件に使うと、通知先未設定時にサーバー確定の診断を取り逃す。PRレビューで指摘された2条件を修正した。

学んだこと

外部入力は「否定されていない」だけで確定扱いせず、必要な条件が明示されていることを確認する。通知の可否と診断の保存条件を独立させ、状態遷移は通知先の設定がなくても観測できるようにする。クライアントから届くフラグは診断情報であり、サーバー自身によるFirestore状態の再検証を意味しない。

設定エラーの誤通知防止と診断情報の保存を修正(未リリース)

bugfix開発中(未リリース)

設定エラーの誤通知防止と診断情報の保存を修正(未リリース)

何を変えたか

設定をFirestoreから購読するとき、キャッシュの欠落・不正データ・未確定書き込みは確認待ちとして扱い、サーバーで確定した不正だけを通知する修正をPRにまとめた。メタデータの変化も購読し、同じデータがサーバーで確定した場合を取り逃さない。

通知Functionは正規化した診断情報をFirestoreとCloud Loggingへ残す。Webhook未設定でも保存し、旧クライアントのキャッシュ由来の通知は抑制する。Git情報のないリモートビルドにも一意の識別子を付ける。

なぜ変えたか

フロント側の変更が反映されても、通知Functionが古いままで診断情報が保存されないケースを確認した。キャッシュだけの情報で設定不正を確定すると誤通知も起きる。過去に破棄された診断情報は復元できないため、当該通知の不正フィールドは確定していない。

Before / After

以前はキャッシュ段階の不正も通知され、通知先の設定がないと保存処理も実行されなかった。修正後はサーバー確定を待ち、通知経路の有無にかかわらず安全な診断情報を残す。未確定の不正データによる購入停止は維持する。

技術的な内容と判断

通常の診断保存・ログは1分間隔、Discord通知は10分間隔で重複を抑える。新しい通知の予約時は即時保存できるため、キャッシュ報告の直後にサーバーで不正が確定しても通知を遅延させない。送信成功時だけ成功時刻を記録し、失敗時は再試行できるようにする。

任意のエラーメッセージや個人情報はログへ渡さない。ビルド識別子は明示設定、Gitリビジョン、UUIDの順で解決する。

学んだこと

App HostingとCloud Functionsは別々のリリース対象として確認する。診断機能は送信するクライアントだけでなく、受信・保存するFunctionの稼働リビジョンまで照合する必要がある。Firestoreのキャッシュからの結果はサーバーでの不存在を意味しない。

Meta validation_statusの実形式に対応

reliability開発中(未リリース)

Meta validation_statusの実形式に対応

変更

PR #102 の再レビューで、Meta Catalog Batch APIの validation_status が文字列だけでなく、次のようなエラーオブジェクト形式を返すケースを取りこぼす指摘があった。

{
  "retailer_id": "185",
  "errors": [
    { "message": "..." }
  ]
}

validationStatusFailed() を修正し、各エントリの errors が非空配列なら失敗として扱うようにした。

実形式を使った回帰テストも追加した。

検証

最新HEADのPR Checkで以下が成功している。

  • Lint app
  • Typecheck app
  • Typecheck Functions
  • Tests

リリース状況

コードはPR #102でstaging、PR #104でmasterへ反映済み。Firebase本番デプロイは未実施のため、ランタイム上は未リリース。

Meta Catalog本番Secret binding修正

reliability開発中(未リリース)

Meta Catalog本番Secret binding修正

問題

本番の processMetaCatalogSyncJobs は起動していたが、デプロイ済みFunctionに META_CATALOG_ACCESS_TOKEN Secretが割り当てられていない状態が確認された。

従来は Secret binding の判定を META_CATALOG_SYNC_ENABLED などのdotenv環境変数に依存させていたため、deploy-time設定として不適切だった。Firebase公式ドキュメントでは通常のdotenv環境変数はdeploy-time設定には利用できないため、deploy対象Project IDを判定基準へ変更した。

修正

  • Meta Secretの条件付き defineSecret() を廃止
  • 本番Project ID mad-tommy のときだけFunction optionsへ文字列で META_CATALOG_ACCESS_TOKEN をbind
  • staging Project ID mad-tommy-stg ではSecret bindingなし
  • Runtimeでは process.env.META_CATALOG_ACCESS_TOKEN から取得
  • dotenv値を逆にしてもProject ID判定が優先されるテストを追加
  • コンパイル済みFunctions entrypointの __endpoint.secretEnvironmentVariables をproduction/staging両方で検証するmanifestテストを追加

Firebase Functions SDKは secrets にSecretParamだけでなくstringも受け付け、endpointの secretEnvironmentVariables へ変換する。

状況

PR #105でstagingへ反映し、PR #106でこのSecret binding修正のみをmasterへ反映済み。PR #106の初回CIは既存の app/character/[id]/page.test.tsx 由来の一時的な window is not defined unhandled errorで失敗したが、全1028テスト自体は成功しており、再実行ではLint / App TypeScript / Functions TypeScript / Testsがすべて成功した。Firebase本番デプロイは未実施。

本番Secretの値自体はコードや記事へ記録しない。

Client error reportingの課金耐性とApp Check導入

security開発中(未リリース)

Client error reportingの課金耐性とApp Check導入

背景

client error reportingはDiscord通知を10分dedupeしていたが、診断状態を毎回Firestoreへ保存する変更により、公開Callableを連打された場合にwrite課金が増える余地があった。

対応

PR #108で多層防御へ変更した。

  • 同一dedupe keyのFirestore state writeを最大1回/分に制限
  • buildIdやdiagnosticsを変えるだけではwrite頻度を増やせない
  • invalidFieldsをcanonical化
  • Discord送信は1分のattempt leaseで多重送信を防止
  • Discord成功後だけlastSentAtを更新
  • webhookは5秒timeout
  • FunctionはmaxInstances=2 / concurrency=10 / timeout=15秒
  • WebクライアントへFirebase App Check / reCAPTCHA Enterprise対応を追加
  • App Check enforcementはFirebase公式推奨どおり段階導入とし、現時点はfalse

App Check rollout

  1. reCAPTCHA Enterprise providerをFirebase App Checkへ登録
  2. App HostingへNEXT_PUBLIC_FIREBASE_APP_CHECK_SITE_KEYを設定
  3. クライアントを配布
  4. App Checkメトリクスで正規トラフィックを確認
  5. productionのCLIENT_ERROR_APP_CHECK_ENFORCEDをtrueへ変更

検証

PR #108の最新CIで以下が成功。

  • Lint app
  • Typecheck app
  • Typecheck Functions
  • Tests

リリース状況

未リリース。staging向けPR #108は未マージ。

Metaカタログの商品表示と商品ページへの遷移を改善

ux開発中(未リリース)

Metaカタログの商品表示と商品ページへの遷移を改善

何を変えたか

Metaカタログ上で色・サイズごとのSKUが大量に並んでも分かりやすく扱えるよう、商品タイトル・グループ・Web遷移を整理した。

  • Metaの商品タイトルから色・サイズを外した
  • linked Itemが1つの場合はProduct名をそのままタイトルにした
  • linked Itemが複数ある場合は「Product名 - Item名」で表示する
  • item_group_idをProduct単位ではなくProduct + Item単位に変更した
  • 色・サイズは同じItem内のバリエーションとしてグループ化する
  • Metaの商品リンクへitem / color / sizeをquery parameterとして付与した
  • MAD TOMMYの商品詳細ページでitem / color / sizeを初期選択として受け取れるようにした

なぜ変えたか

従来は、たとえば5色×4サイズの商品なら20SKUそれぞれに「商品名 - 色 - サイズ」というタイトルが付いていた。

また、異なるlinked Itemも同じProduct由来という理由だけで同じitem_group_idにまとめていたため、Tシャツとサコッシュのような別ボディまで同じバリエーション群として扱われる可能性があった。

Meta上ではSKUは個別に必要だが、購入者に見せる単位はもっと整理した方が分かりやすい。

ユーザーにとって何が良くなったか

Instagram / Facebook側では、色・サイズの違いをタイトルへ大量に埋め込まず、同じ商品グループのバリエーションとして扱いやすくなる。

また、Meta側からWebサイトへ進んだ場合、該当するItem・色・サイズをURLへ引き継ぎ、MAD TOMMYの商品詳細画面でその選択状態を初期表示できる。

技術的な内容

変更前:

MAD TOMMY ロゴ - ブラック - M
MAD TOMMY ロゴ - ブラック - L
MAD TOMMY ロゴ - ホワイト - M
...
item_group_id = Product単位

変更後:

MAD TOMMY ロゴ - Tシャツ
  ├─ ブラック / M
  ├─ ブラック / L
  └─ ホワイト / M

MAD TOMMY ロゴ - サコッシュ
  └─ ナチュラル

item_group_id = Product + Item単位

linked Itemが1つだけなら、タイトルはさらに簡潔にProduct名だけにする。

Metaへ送るlinkも次の形にした。

https://madtommywith.com/shop/{productId}
  ?item={itemId}
  &color={color}
  &size={size}

商品詳細ページでは既存のitem queryに加え、color / sizeも初期選択値として受け取る。

Before / After

Before

  • SKUごとに色・サイズ付きタイトルが並ぶ
  • 複数linked Itemも同一item_group_id
  • Metaからサイトへ遷移してもItem・色・サイズの選択状態は引き継がれない

After

  • 商品タイトルを簡潔化
  • Item単位でグループを分離
  • 色・サイズは属性として管理
  • Metaで見ていたバリエーションをMAD TOMMYの商品ページへ引き継ぐ

判断したこと・トレードオフ

  • カタログ内部のSKU数自体は減らさない。在庫・価格を色・サイズ単位で正しく持つため。
  • 見た目の重複はtitleとitem_group_idで整理する。
  • linked Itemが異なる場合は別商品グループとし、色・サイズだけを同一グループ内のvariationとする。
  • Web側でもquery parameterを受け取ることで、外部カタログからの導線を商品トップではなく該当variantに近づけた。

ハマったこと・学んだこと

  • カタログ内部の「SKU件数」とInstagram上で購入者が認識する「商品数」は分けて考える必要がある。
  • item_group_idの粒度を粗くしすぎると、意味の違うItemまで同じvariantとしてまとめてしまう。
  • 外部カタログのlinkは単なる商品URLではなく、variant情報をqueryで引き継ぐと購入導線を短くできる。
  • Metaが管理するFacebook for WooCommerceでも、items_batch用にtitle / image_link / link / priceなどを明示的に送る実装になっている。

Meta Catalog APIで商品・在庫を自動同期する基盤を実装

featureリリース済み

Meta Catalog APIで商品・在庫を自動同期する基盤を実装

何を変えたか

MAD TOMMY WITHの商品データをMetaの商品カタログへ自動同期する基盤を実装した。

今回追加した主な処理は次の通り。

  • Product更新時に、対応するMeta商品を同期するFirestoreトリガー
  • Itemの価格・在庫・画像変更時に、紐づくProductを再同期するFirestoreトリガー
  • 30分ごとのreconciliation(定期的な整合性確認)
  • Meta Catalog APIの /{catalog-id}/items_batch クライアント
  • Product × linked Item × Color × SizeをMetaの商品バリエーションへ変換するProjection
  • 商品ごとの同期状態・ハッシュをFirestoreへ保存し、変更がない場合はAPI送信を省略する差分同期
  • 公開停止・削除されたバリエーションをMeta側からDELETEする処理
  • Meta APIのアクセストークンをFirebase Secret Managerで扱う構成
  • 本番Firebase project以外では同期を有効化しないガード
  • APIクライアント、商品変換、差分同期、環境設定のテスト

実装はPR #95で管理している。

リリース状況: リリース済み。
Meta Access Tokenの本番Secret登録、staging→master反映、Firebase Functions / Firestore Rulesの本番デプロイまで完了した。

なぜ変えたか

自社ECの商品をMetaの商品カタログでも利用する場合、管理画面で商品名・価格・画像・在庫を二重入力すると更新漏れが起きやすい。

MAD TOMMY WITHでは、商品ページを表すProductと、実際のボディ・色・サイズ・価格・在庫を持つItemを分けて管理している。

Product
  └─ linked Item
       ├─ Black / M
       ├─ Black / L
       ├─ White / M
       └─ White / L

この既存商品マスタを正本としてMetaへ同期すれば、EC側で商品や在庫を変更したときにMeta側も追従できる。

ユーザーにとって何が良くなったか

本番リリース後は、EC側の商品情報を更新すればMetaの商品カタログへ自動反映できるようになる。

管理者側では、Meta用に同じ商品情報を手動で入力・更新する作業を減らせる。価格・在庫・画像の変更もItem更新を起点に再同期される。

将来的にはInstagramやFacebook側の商品表示・広告に、同じ商品マスタを利用しやすくなる。

技術的な内容

既存のMerchant Projectionを再利用した

すでにGoogle Merchant Center向けに、Productとlinked Itemを色・サイズ単位のofferへ展開するロジックが存在していた。

Meta向けに同じバリエーション生成ルールをもう一度実装すると、チャネルごとにSKUや在庫判定がずれる可能性があるため、既存Projectionを再利用してMeta形式へ変換するAdapterを追加した。

Firestore Product + Item
        ↓
既存 Merchant Projection
        ↓
共通の安定ID・価格・在庫・画像
        ↓
Meta向けフィールド名へ変換
        ↓
Catalog API items_batch

これにより、色・サイズの識別子は既存同期と同じ安定したハッシュベースIDを使える。

Items Batch APIでUPDATEをupsertとして使う

Metaが管理しているFacebook for WooCommerceの実装では、Product CatalogのItems Batch APIへ次のような構成で送信している。

POST /{catalog-id}/items_batch

allow_upsert=true
item_type=PRODUCT_ITEM
requests=[
  {
    method: "UPDATE",
    data: {
      id: "...",
      ...
    }
  }
]

allow_upsert=true にすることで、同じUPDATE経路で未登録商品の作成と既存商品の更新を扱える。

削除は次の形にする。

{
  method: "DELETE",
  data: {
    id: "..."
  }
}

商品追加用と商品更新用のコードパスを分けず、安定IDを基準に同期できる構成にした。

Meta向けフィールドへ明示的に変換する

Meta側には次のようなfeed形式のフィールドを送る。

id
title
description
availability
condition
price
link
image_link
brand
item_group_id
color
size
age_group
gender

たとえば内部では価格をmicro単位で扱う既存Projectionを利用しているが、Metaへ送る直前に 4800 JPY のような文字列形式へ変換する。

不正な価格や必須項目不足を固定値で埋めて送信することはせず、その商品をinvalidとして同期状態へ記録する。

ProductだけでなくItem変更も同期トリガーにする

価格・在庫・色別画像はProductではなくItem側にあるため、Product更新だけ監視しても在庫変更がMetaへ反映されない。

そこで次の2経路を用意した。

products/{productId} 更新
  ↓
そのProductを即時同期

items/{itemId} 更新
  ↓
そのItemをlinkedItemsに持つProductを検索
  ↓
対象Productを再同期

現在のFirestore構造ではnestedなlinked Item IDをそのまま効率よく逆引きできないため、Item更新時はProduct一覧から関連Productを抽出する。商品数が大きくなった場合は逆引きインデックス用コレクションを持つ余地がある。

差分同期用の状態をFirestoreへ保存する

metaCatalogSyncState/{productId} に、Metaへ送信済みの商品IDと内容ハッシュを保存する。

次回同期時に同じハッシュならAPI呼び出しを省略し、変更されたバリエーションだけUPDATEする。

以前同期したIDが現在のProjectionから消えた場合はDELETEする。

同期状態コレクションはクライアントから直接読書きできないFirestore Rulesにしている。

30分ごとのreconciliationを入れた

Firestoreトリガーだけに依存すると、一時的な外部API障害などで同期を取りこぼす可能性がある。

そのため30分ごとに、現在のProductと同期状態を突き合わせて再確認するFunctionを追加した。

通常:
Firestore更新 → 即時同期

補完:
30分ごと → 全Product / 過去の同期状態を確認 → 必要な差分を同期

トリガーで低遅延に反映しつつ、定期処理で最終的な整合性を補完する構成にした。

アクセストークンはソースコードへ保存しない

Metaのアクセストークンは環境設定ファイルやGitHubへコミットせず、Firebase Secret Managerの META_CATALOG_ACCESS_TOKEN としてCloud Functionsへbindする。

カタログIDやAPIバージョンのような秘密ではない設定だけを環境設定として管理する。

また、同期機能は次の条件をすべて満たした場合だけ有効になる。

META_CATALOG_SYNC_ENABLED=true
AND Firebase project == production
AND Catalog IDが有効
AND Graph API versionが有効

stagingでは同期フラグをfalse、Catalog IDを空にしている。

エラー本文やTokenをログへ残さない

外部APIのエラー本文には任意の文字列が含まれる可能性があるため、そのままFirestoreやログへ保存しない。

HTTP statusとMetaの数値error code / subcodeから、固定形式のエラーコードだけを生成する。

META_190_463
HTTP_500
INVALID_RESPONSE

AuthorizationヘッダーにTokenを入れ、URLのquery parameterには含めない。

Before / After

Before

  • Metaの商品カタログは手動登録が前提だった。
  • ECの商品・価格・在庫・画像をMetaへ自動反映する経路がなかった。
  • Meta向けの認証情報を安全にCloud Functionsへ渡す仕組みがなかった。

After

  • ECの商品マスタからMeta商品を自動生成できるコードが追加された。
  • Product / Item変更を起点に自動同期できる。
  • 差分だけ更新し、削除されたバリエーションもMeta側から削除できる。
  • 定期reconciliationで同期漏れを補完できる。
  • TokenはFirebase Secret Managerで管理し、Gitには保存しない。
  • CIでAppとFunctionsのTypeScript型チェック、既存テスト、新規Meta同期テストを確認する。

判断したこと・トレードオフ

  • CSVアップロードではなくAPI同期にした。商品マスタをEC側へ一本化したいため。
  • Graph API Explorerでの手動テストを本番実装の前提にせず、API境界をmockした自動テストとCIを追加した。
  • バリエーション生成をMeta専用に複製せず、既存Merchant Projectionを再利用した。チャネル間でID・在庫判定がずれるリスクを下げられる。
  • ItemからProductへの逆引きは現状全Productを確認する。現在の規模では単純だが、商品数が増えれば逆引き用データ構造へ変更する余地がある。
  • 有効期限付きアクセストークンを利用する場合は、期限切れ前にSecretをローテーションする必要がある。

ハマったこと・学んだこと

  • Metaの商品カタログAPIは、一般的な「APIキーを貼って単品PUTする」だけではなく、Business Portfolio、App、System User、Catalog、権限、Access Tokenの関係を準備してから利用する。
  • Catalog APIの実装時は、公式にMetaが保守しているFacebook for WooCommerceの現行実装も、実際のItems Batchリクエスト形を確認する参考になる。
  • 外部カタログ同期では「作成・更新」だけでなく、公開停止・バリエーション削除のDELETE経路が必要。
  • Productの変更だけを監視すると、別コレクションにある価格・在庫の更新を取りこぼす。正本データが複数ドキュメントへ分かれている場合は、どの変更を同期トリガーにするかを先に洗い出す必要がある。
  • 外部API同期は即時トリガーだけで完結させず、定期reconciliationを組み合わせると一時障害の補完経路を作れる。

Meta / Google Merchant同期の後追いレビュー修正

reliability開発中(未リリース)

Meta / Google Merchant同期の後追いレビュー修正

概要

既存のMeta Catalog / Google Merchant同期を後追いレビューし、同期成功判定、再試行、日次照合、Item更新反映、staging deployのSecret入力問題を修正した。

この変更はPR #102はstagingへマージ済みで、PR #104でmasterへ反映済み。Firebase本番デプロイは未実施。

主な修正

  • Metaのitems_batchレスポンスでvalidation_statusを確認
  • API受付直後はsubmittedとして扱い、Meta側の商品存在確認後にverifiedへ進める
  • Meta側から欠損した商品を再送
  • DELETE完了確認までpendingDeleteIdsを保持
  • Meta同期をFirestore queue化し、429 / 5xx / network errorをretry
  • remote verificationにも上限回数を設定
  • Google Merchant workerを毎時実行・最大20商品に変更
  • Google MerchantはProduct更新だけでなくItem更新でも同期queueへ積む
  • Product↔Item逆引き用catalogSyncLinksを追加
  • 初回reverse index backfill完了前は部分indexを信用せず全Productを確認
  • Meta deep linkのquery変更に商品画面が追従
  • 内部queue/indexコレクションをFirestore Rulesでクライアント非公開化

staging deployのSecret入力問題

stagingではMETA_CATALOG_SYNC_ENABLED=falseにもかかわらず、Firebase CLIがdefineSecret("META_CATALOG_ACCESS_TOKEN")を検出するとSecret Manager上の値を解決しようとして入力を要求していた。

stagingではSecretをFunctionへbindしないだけでなく、defineSecret自体を宣言しないよう変更した。本番ではenabled=trueのため従来どおり宣言・bindする。

検証

PR #102 最新HEADのGitHub Actionsで以下が成功している。

  • Lint app
  • Typecheck app
  • Typecheck Functions
  • Tests

リリース状況

コードはstaging / masterへマージ済み。Firebase本番デプロイは未実施のため、ランタイム上は未リリース。

GitHub Copilot reviewerをREST API経由で依頼したが、このリポジトリではrequested reviewerとして登録されず、別エージェントレビューは未完了。

Meta Catalog / Google Merchant同期の定期実行を省リソース化

optimizationリリース済み

Meta Catalog / Google Merchant同期の定期実行を省リソース化

何を変えたか

外部商品カタログ同期の定期実行頻度を見直し、アイドル時のCloud Scheduler / Cloud Functions / Firestore利用を減らした。

変更内容は次の通り。

  • Meta Catalogの全件reconciliationを「30分ごと」から「1日1回(03:40 JST)」へ変更
  • Meta Catalogで商品差分がない場合、同期状態の checkedAt だけを書き換えるFirestore Writeを廃止
  • Google Merchantのqueue workerを「15分ごと」から「6時間ごと(0:00 / 6:00 / 12:00 / 18:00 JST)」へ変更
  • Google Merchantの日次reconciliation(03:20 JST)は維持
  • Functionsのスケジュール契約テストも新しい頻度へ更新

ブランチ運用は、変更を先にstagingへマージした後、stagingからmasterへ反映した。最終的にstagingとmasterは同一コミットへ揃えている。

リリース状況: リリース済み。
省リソース化したスケジュール設定をstaging→masterへ反映し、Firebase Functionsへ本番デプロイ済み。

なぜ変えたか

MAD TOMMY WITHの商品数では、数十分単位の全件整合性確認を常時回す必要性が低い。

通常のMeta商品変更はFirestoreトリガーで同期できるため、全件reconciliationは同期漏れを補完する保険として1日1回で十分と判断した。

Google Merchantはoutbox形式のqueue workerを利用しているため完全に日次1回へ落とすと、失敗ジョブや通常更新の反映が長時間遅れる。そこで日次reconciliationは維持しつつ、queue workerだけ6時間間隔に落とした。

ユーザーにとって何が良くなったか

購入者向けUIの変更はない。

運用面では、商品数が少ない段階で不要な定期実行を抑えられるため、Firebase / Google Cloudの利用量と課金リスクを減らせる。

技術的な内容

Metaはイベント駆動 + 日次reconciliationにした

変更前:

商品変更 → Firestore Trigger → Meta同期

30分ごと
  ↓
全Product / Sync Stateを読み取り
  ↓
整合性確認

変更後:

商品変更 → Firestore Trigger → Meta同期

1日1回 03:40
  ↓
全Product / Sync Stateを読み取り
  ↓
整合性確認

全件reconciliationのScheduler起動回数は、1日48回から1回へ減る。

差分なしのFirestore Writeをやめた

以前はAPI送信が不要な商品でも、確認した証跡として checkedAt を更新していた。

差分なし
  ↓
Meta APIは呼ばない
  ↓
checkedAtだけFirestoreへWrite

このWriteは同期結果そのものを変えないため廃止した。

変更後は差分がない場合、そのまま unchanged を返してFirestoreへ書き込まない。

Google Merchantはqueue workerだけ低頻度化した

Google Merchant同期はMetaと異なり、Firestore更新時にoutboxへジョブを積み、別のworkerが処理する構成になっている。

そのためworkerを完全に削除せず、次の頻度へ変更した。

Before:
15分ごと = 96回/日

After:
6時間ごと = 4回/日

日次reconciliationは03:20 JSTのまま維持している。

これにより、定期workerの起動回数は約95.8%削減される一方、queueの回収経路は残る。

staging → masterの順序を固定した

今回の変更は次の順で反映した。

feature branch
  ↓
PR #96 → staging
  ↓
PR #97 → master
  ↓
staging / masterを同一HEADへ同期

今後も本番変更はstagingへ先に反映・確認し、その後masterへ進める。

Before / After

Before

  • Meta全件照合: 48回/日
  • Meta差分なし商品でもFirestore Write
  • Google Merchant queue worker: 96回/日
  • Google Merchant日次reconciliation: 1回/日

After

  • Meta全件照合: 1回/日
  • Meta差分なし商品: Writeなし
  • Google Merchant queue worker: 4回/日
  • Google Merchant日次reconciliation: 1回/日

判断したこと・トレードオフ

  • Metaは通常変更をイベント駆動で処理できるため、全件確認を日次まで落とした。
  • Google Merchantはqueue方式なので、workerを完全に日次1回にはしなかった。
  • Google Merchantのworkerを6時間ごとへ落としたため、queueに残ったジョブの回収は最大で数時間遅れる可能性がある。
  • コスト削減を優先しつつ、日次reconciliationとqueue workerという2つの復旧経路は残した。

ハマったこと・学んだこと

  • 外部API同期のコストを考えるときは「API呼び出し回数」だけでなく、差分がなくても発生するScheduler起動、Firestore Query、状態更新Writeも確認する必要がある。
  • 即時反映が必要な処理はイベント駆動にし、定期実行は整合性確認・リカバリ用途へ限定すると無駄を減らしやすい。
  • queue workerは単純に削除するとretry経路も消えるため、同期方式ごとに役割を確認して頻度を落とす必要がある。

LINE内ブラウザ向けにGoogleログインをredirect方式へ変更

bugfix開発中(未リリース)

LINE内ブラウザ向けにGoogleログインをredirect方式へ変更

何を変えたか

Googleログインを signInWithPopup から signInWithRedirect へ変更した。ログイン画面へ戻った際は getRedirectResult で認証結果を回収し、成功時に元の遷移先へ進む。

なぜ変えたか

通常のiPhone Safariでは再現しなかったが、LINE内ブラウザではGoogleログイン成功後に madtommywith.com へ戻ったところで白画面になることが確認できた。

Firebase公式ドキュメントではGoogleログインのredirect方式をモバイル端末で推奨している。ポップアップ方式は端末やプラットフォームによってブロックされたり、モバイルで操作が滑らかでない場合がある。

ユーザーにとって何が良くなったか

Googleログイン時に別タブ・ポップアップへ依存せず、現在のブラウザ画面自体をGoogle認証へ遷移させて戻すため、LINEなどのアプリ内ブラウザでも認証完了後に元画面へ復帰しやすくなる。

技術的な内容

認証開始時:

ログイン画面
  -> signInWithRedirect
  -> Google認証画面

認証完了後:

Google
  -> <app-domain>/__/auth/handler
  -> ログイン画面
  -> getRedirectResult
  -> nextで指定された画面

前段の修正で /__/auth/* をアプリと同一オリジン経由でFirebaseへリバースプロキシしているため、Safariなどでredirect認証を使う際の第三者ストレージ制限対策も併用している。

Before / After

Before

  • signInWithPopup を利用。
  • LINE内ブラウザではGoogle認証後にアプリへ戻った際、白画面になるケースがあった。
  • ポップアップまたは別タブと元画面の連携に依存していた。

After

  • signInWithRedirect を利用。
  • Google認証後はログイン画面へ戻り、getRedirectResult で結果を回収する。
  • 成功後は元の next パスへ進む。

判断したこと・トレードオフ

  • デスクトップではpopup方式も使いやすいが、今回はLINE内ブラウザを含むモバイル互換性を優先してredirectへ統一した。
  • redirect方式はページ全体がGoogleへ移動するため画面遷移が発生するが、ポップアップ管理に依存しない。
  • Safari等でredirect方式を安定させるため、同一オリジンの認証ヘルパー構成を前提とする。

ハマったこと・学んだこと

  • 通常Safariで再現しない認証不具合でも、LINE内ブラウザのようなWebViewでは別ウィンドウやOAuth復帰の扱いが異なる。
  • 「Google認証は成功しているが、アプリへ戻ると白画面」という症状は、認証プロバイダそのものより復帰方法やブラウザコンテキストを疑う必要がある。
  • モバイルのOAuthでは、popupとredirectを単純に同等と考えず、利用されるブラウザ環境まで含めて選ぶ必要がある。

リリース状況

未リリース。2026-09-28時点では修正PR #93をステージングへマージ済み。本番反映はLINE内ブラウザでの再確認後。

Firebase Auth の認証ヘルパーを同一オリジン経由にする

bugfix開発中(未リリース)

Firebase Auth の認証ヘルパーを同一オリジン経由にする

何を変えたか

Googleログイン後、一部ブラウザで認証用ポップアップが白画面のまま進まない問題への対策として、Firebase Authentication が利用する /__/auth/* をアプリと同じオリジンで受け、各Firebaseプロジェクトの firebaseapp.com へ透過的にリバースプロキシする構成へ変更した。

本番では Firebase の authDomain をアプリ本体のドメインへ、ステージングではステージングの App Hosting ドメインへ合わせた。Googleログイン方式自体は signInWithPopup のままとし、認証ヘルパーのオリジンだけを揃えている。

なぜ変えたか

従来はアプリ本体と Firebase Auth のカスタム認証ドメインが別オリジンだった。Googleアカウント選択までは進めても、その後に認証ヘルパーへ戻る段階でブラウザによって白画面になる現象が発生していた。

Firebase公式ドキュメントでは、Firebase Hosting以外でアプリを配信する場合に、アプリドメインの /__/auth/ を <project>.firebaseapp.com/__/auth/ へ透過プロキシし、authDomain をアプリドメインへ合わせる方法が案内されている。

ユーザーにとって何が良くなったか

ブラウザごとのサードパーティストレージ制限や認証ヘルパーの別オリジン依存を減らせるため、Googleログインのブラウザ互換性向上が期待できる。

技術的な内容

Next.js の rewrites() を利用し、次の経路で認証ヘルパーを配信する。

Browser
  -> https://<app-origin>/__/auth/*
  -> Next.js rewrite
  -> https://<firebase-project>.firebaseapp.com/__/auth/*

FirebaseプロジェクトIDはビルド時の環境変数から取得し、ステージングと本番で同じ next.config.mjs を利用できるようにした。

Googleログインの signInWithPopup は変更していない。問題の焦点をログイン方式ではなく認証ヘルパーのオリジンに絞ることで、既存のログイン後処理への影響を最小限にした。

Before / After

Before

  • アプリ本体と認証ヘルパーが異なるオリジンだった。
  • Googleアカウント選択後、一部ブラウザでポップアップが白画面になることがあった。
  • 認証ドメインとして専用サブドメインを利用していた。

After

  • /__/auth/* をアプリ本体と同一オリジンで公開する。
  • Next.js が Firebase Hosting の認証ヘルパーへ透過プロキシする。
  • authDomain を各環境のアプリ配信ドメインへ揃える。

判断したこと・トレードオフ

  • signInWithRedirect への全面移行は行わず、既存の signInWithPopup を維持した。ログイン後の画面遷移や状態管理を変更せず、問題箇所だけを小さく修正するため。
  • 302リダイレクトではなくリバースプロキシを採用した。Firebase公式ドキュメントでも、ブラウザから別オリジンに見えない透過プロキシが必要とされている。
  • OAuth側の承認済みリダイレクトURIとFirebase AuthenticationのAuthorized domainsはコードだけでは完結しないため、環境ごとの管理画面設定が必要。

ハマったこと・学んだこと

  • Firebase Auth のポップアップ画面そのものが開いていても、OAuth完了後に戻る認証ヘルパーの配信経路が正しくなければ、アプリ側では白画面に見えることがある。
  • 「ブラウザによって動く・動かない」という認証問題では、ログインAPIだけでなく、authDomain、/__/auth/handler、サードパーティストレージ制限、OAuthのリダイレクトURIまで一続きで確認する必要がある。
  • ステージングと本番でFirebaseプロジェクトが異なる場合、リバースプロキシ先を固定値にせず、プロジェクトIDから組み立てることで設定の重複を減らせる。

リリース状況

未リリース。2026-09-28時点では修正PRを作成済みで、ステージング反映前。

eBay Inventory API連携の土台を実装し、バリエーション出品設計をレビュー

feature開発中(未リリース)

eBay Inventory API連携の土台を実装し、バリエーション出品設計をレビュー

何を変えたか

MAD TOMMY WITHの商品マスタを外部マーケットプレイスへ展開するため、既存のマーケットプレイス固有実装を整理し、eBay Sell Inventory API向けの連携基盤へ置き換えた。

今回の段階では本番出品までは接続せず、次の土台を実装した。

  • eBay用Item設定(連携ON/OFF、カテゴリID)
  • OAuth refresh tokenからUser access tokenを取得する認証処理
  • Production / Sandboxを切り替えられるInventory APIクライアント
  • SKUごとのInventory Item作成・更新
  • Offer作成と単一SKUのPublish
  • Inventory Item Group作成・更新
  • 複数バリエーションのGroup Publish
  • Product × linked ItemからeBay向けデータを作るProjection
  • 安定したSKU / Inventory Item Group Keyの生成
  • eBay APIの数値errorIdだけを安全な固定コードへ変換
  • eBay専用CI
  • 本番Firebase project以外では連携を有効化しないガード

現時点では EBAY_ENABLED=false のままで、Firestoreトリガーや定期同期も接続していないため、eBay本番環境への書き込みは発生しない。

なぜ変えたか

自社ECの商品・在庫データを二重管理せず、外部マーケットプレイスにも展開できる構成を作りたかった。

MAD TOMMY WITHでは、1つの商品デザインに複数の本体Itemが紐付き、さらに各Itemが色・サイズ・在庫を持つ。

Product
  └─ linked Item
       ├─ Black / M
       ├─ Black / L
       ├─ White / M
       └─ White / L

そのため、単純な「1商品 = 1出品」ではなく、eBayの複数バリエーション商品として表現できる設計が必要だった。

また、外部API連携は一度本番同期を有効にすると、誤った在庫・価格・商品情報を外部へ送る可能性がある。そこで、アカウント開設前はAPIクライアントと変換ロジックまで作り、本番書き込み経路は意図的に接続しない方針にした。

ユーザーにとって何が良くなったか

まだ開発段階のため、購入者向けUIへの直接的な変更はない。

将来的には、自社ECで管理する商品・価格・在庫をeBayへ同期し、複数チャネルの在庫管理を一本化できる。

管理者側では、ItemごとにeBay連携対象とカテゴリIDを設定できる土台を用意した。

技術的な内容

複数バリエーションではInventory Item Groupが必要

最初の実装は次の単一SKU向けフローまでだった。

Inventory Item
  ↓
Offer
  ↓
Publish

レビュー時に公式ドキュメントを再確認すると、色・サイズを1つの商品ページへまとめるにはInventory Item Groupが必要だった。

複数バリエーションの流れは次の形になる。

SKUごとにInventory Itemを作る
  ↓
Inventory Item GroupへSKUをまとめる
  ↓
SKUごとにOfferを作る
  ↓
publishOfferByInventoryItemGroup
  ↓
1つの複数バリエーションListing

そのためレビュー後に createOrReplaceInventoryItemGroup と publishOfferByInventoryItemGroup をAPIクライアントへ追加した。

Projection側にも groupKey、共通aspects、variesBy.specifications、aspectsImageVariesBy を持たせ、色・サイズのvariation構造を明示するようにした。

SKUは内部IDから安定生成する

商品名からSKUを生成すると名称変更で識別子が変わるため、Product ID / Item ID / Color / SizeをSHA-256へ入力して固定長の識別子を生成する。

mtw_<Product+Item hash>_<Color+Size hash>

表示名や翻訳文が変わってもSKUを維持できる。

OAuthはUser tokenとApplication tokenを区別する

Seller固有の在庫・Offerを操作するInventory APIではAuthorization Code GrantによるUser tokenを使う。

一方、TaxonomyやMetadataのようなユーザー固有ではない情報はApplication tokenを使うAPIがある。

今回のInventory API認証は次の構成。

Client ID / Client Secret
       ↓
Authorization Code Grantで取得したRefresh Token
       ↓
短命なUser Access Tokenを更新
       ↓
Inventory API

現時点の自動処理はInventory APIだけなので、OAuth scopeも sell.inventory のみに絞った。Business PoliciesはIDを設定値として受け取るため、まだ sell.account は要求しない。将来Account APIでPolicy自体を自動操作する段階で追加する。

Access tokenは有効期限内だけメモリキャッシュし、refresh tokenやclient secretはソースコードへ保存しない。

APIエラー本文をそのまま保存しない

外部APIのエラー本文をログやDBへ丸ごと保存する設計は避けた。

eBayレスポンスから数値errorIdだけを取り出し、EBAY_25002 のような固定コードへ変換する。

これにより、同期ジョブのエラー分類には利用できる一方、任意のレスポンス本文を保存しない。

入力境界でカテゴリIDを検証する

管理画面から入力するeBayカテゴリIDは normalizeItem() で数値だけに正規化する。

管理画面
  ↓
normalizeItem()
  ↓
eBay設定検証
  ↓
Projection
  ↓
eBay API

UI入力をそのまま外部APIへ渡さない。

production project以外では有効化できない

EBAY_ENABLED=true だけでは同期を有効化しない。

EBAY_ENABLED=true
AND
Firebase project == mad-tommy
AND
Location / 3 Business Policies / 価格レートが設定済み

の場合のみconfigを有効と判定する。

実際にFirebase Functionsへ同期を接続するときは、built-in projectID と omit も利用し、eBay Secretをproduction Functionだけへbindする予定。

Before / After

Before

  • eBayの単一SKU Publishフローは作れていたが、色・サイズを1つのListingへまとめるGroup APIが不足していた。
  • 外部APIの本番有効化条件が設計途中だった。
  • 旧マーケットプレイス固有コードが残っていた。

After

  • eBay向け境界を functions/src/ebay/ に分離した。
  • Inventory Item Groupを使う複数バリエーション設計に修正した。
  • OAuth、Inventory API、Config、Projectionを個別テストできる。
  • Production Firebase project以外ではeBay同期configが有効にならない。
  • 旧マーケットプレイス用コードをstagingから削除した。
  • 本番自動同期は未接続で、安全に停止している。

判断したこと・トレードオフ

同期Coordinatorはアカウント開設後に実装する

eBayではInventory Location、Payment / Fulfillment / Return Policy、対象Marketplace、Category、OAuth User tokenなどアカウント固有値が必要になる。

実値がない状態で同期Coordinatorまで完成させると推測した仕様を固定化しやすいため、今回は認証・API client・Projection・Unit Testまでで止めた。

単純為替で販売価格を決めない

JPY価格からUSDへ変換する設定値は用意したが、本番では市場為替だけで販売価格を決めない。

eBay販売手数料、国際送料、為替変動、返品リスク、利益率を含む価格戦略を決めるまで価格レートは空のままとする。

ハマったこと・学んだこと

endpoint単体ではなく公式ワークフローも読む

Inventory ItemとOfferのAPIだけを見ると、複数SKUを作れるためバリエーション出品まで完成したように見えた。

しかし、1つのListingで色・サイズを選べるようにするにはInventory Item Groupが必要だった。

さらにInventory Item Groupでは、variesBy.specifications で定義した可変aspectと各SKUの product.aspects を一致させ、共通aspectはGroup側へ分離する必要がある。画像についても aspectsImageVariesBy とGroup画像の整合が必要になる。

MAD TOMMYでは色別画像のマッピングは既に持っているため、Color variationでは各色の colorImageUrls を必須にした。一方、サイズ別画像の明示的な対応情報は持っていない。Inventory Item Groupでは画像variation aspectとGroup画像が必要になるため、サイズだけが可変の商品へ汎用画像を適当に割り当てるのではなく、現段階では sizeVariationImages として送信対象外にするfail-closed設計にした。

外部APIでは個別endpointリファレンスだけでなく、公式のユースケース・必須フィールド・エラー詳細まで確認する必要がある。

createOrReplaceは差分更新とは限らない

Inventory Item Group更新では、現在のdesired stateを再生成して送る設計が扱いやすい。

部分的な差分だけを考えると、GroupからSKUを意図せず外すなどの事故につながり得る。

Offer IDを保持しないと冪等更新できない

新規時はcreateOfferでよいが、毎回createすると重複Offerの原因になる。

本番同期では次の対応状態をFirestoreへ保存する必要がある。

MAD TOMMY Product / Item / SKU
  ↔
eBay Inventory Item / Group / Offer ID / Listing ID

これを基にcreate / update / withdraw / deleteを判定する。

カテゴリによってPublish要件が変わる

Inventory APIの型だけ満たしていても、Publish時にCategory固有条件で失敗する可能性がある。

本番前にはTaxonomy / Metadata APIでCategory、Product Identifier(GTIN / UPC / MPN等)、Condition、必須Item Specificsを確認する。

英語化は商品名・説明だけでは足りない

商品名と説明は既存の英語翻訳を優先するようにしたが、Item masterの色・サイズは現在の値をそのまま利用している。

eBay US向けにはカラー名・サイズ名についても英語表現を持たせるか、変換辞書を用意する必要がある。

購入不能時の診断と匿名エラー通知を追加

feature開発中(未リリース)

購入不能時の診断と匿名エラー通知を追加

何を変えたか

購入に必要なFirestore設定が不正な場合、ユーザーには内部事情を含まない一時停止メッセージを表示し、開発者コンソールには不正フィールドと修正先を記録するようにした。あわせて、設定・カタログ読込、注文作成、決済セッション作成、配送先保存、問い合わせ、レビューなどの操作でシステムエラーが起きた際、既存のDiscord Webhookへ匿名の固定メタデータだけを通知するCloud Functionを追加した。

なぜ変えたか

設定ドキュメントに新しい必須項目が不足したことで購入不能になった際、従来の画面は「配送設定が完了していない」と原因を狭く誤認させる表示だった。また、ユーザー操作で発生したエラーを運用者が能動的に把握する経路が不足していた。

ユーザーにとって何が良くなったか

内部設定の詳細や誤った原因説明を見せず、再試行可能な一般メッセージで案内できる。問い合わせ送信失敗時は入力内容を保持するため、再入力の負担も減る。運用者は通知とコンソール情報から問題箇所を早く特定できる。

技術的な内容

  • Firestoreの生設定をCloud Functions側と同等の条件で診断し、フィールドパス単位の問題一覧を返す。
  • ブラウザから送れる通知データを、操作種別・エラーコード・固定画面パス・ビルドIDの列挙型に限定する。
  • Cloud Functionでも入力を再検証し、任意フィールドは破棄する。
  • Discord payloadにはユーザーID、氏名、メール、住所、注文ID、入力値、例外メッセージ、スタックトレースを含めない。
  • 操作・コード・画面パスからSHA-256キーを作り、Firestore transactionで同一エラーを10分間重複通知しない。
  • 通知自体の失敗は元のユーザー操作へ伝播させない。
ユーザー操作
  -> 固定エラー分類
  -> Callable Functionで再検証
  -> Firestoreで10分重複抑制
  -> 既存Discord Webhookへ匿名通知

Before / After

Before

  • 購入不能の理由を配送設定に限定して表示していた。
  • 設定不正の具体的なフィールドをログから特定しづらかった。
  • クライアント操作エラーの通知経路がなかった。

After

  • ユーザー表示と運用者向け診断を分離した。
  • 管理画面の修正場所と不正フィールドをコンソールで特定できる。
  • 個人情報を含まない固定メタデータだけを通知できる。

判断したこと・トレードオフ

  • 例外メッセージは診断に便利だが個人情報や内部情報を含み得るため、Webhookへ送らず固定コードへ変換した。
  • 入力ミス、在庫切れ、ユーザーキャンセルなど通常の業務結果は通知対象外とし、対応可能なシステム・権限・設定エラーだけを対象にした。
  • 現状クライアントでFirebase App Checkを初期化していないため、今回のCallable Functionだけ強制すると正規ユーザーの通知も失敗する。固定allowlistと重複抑制を先に採用し、App Check導入は別課題とした。

ハマったこと・学んだこと

  • 表示用の設定正規化が既定値を補完していても、注文作成側が生データを厳格検証すると両者の判定がずれる。設定スキーマ追加時は、保存処理・生データ診断・サーバー検証・既存データ移行を一体で扱う必要がある。
  • 通知APIへ例外オブジェクトを渡す設計は、意図せず個人情報を送る危険がある。クライアントとサーバーの両方で固定スキーマへ絞る方が安全だった。
  • 重複抑制キーを有限な列挙値だけから作れば、保存ドキュメント数も上限が決まり、任意文字列による無制限増加を避けられる。

my-card(chicchi.work)

Markdownで管理した技術記事、日次ITニュース、プロダクトの更新情報を、Astroで静的なWebページと配信用データに変換するサイト。生成したファイルをFirebase Hostingから配信する。

公式ページ

記事側だけのRunner移行ではサイト再ビルドが復旧しなかった

ci未リリース

記事側だけのRunner移行ではサイト再ビルドが復旧しなかった

リリース状況

未リリース。本番およびPRプレビューのRunnerをセルフホストへ変更するPR #20を作成したが、この記録時点ではプレビューがqueuedのままで、ジョブのステップは開始されていない。未検証のままmasterへマージしていない。ニュース50件は別リポジトリのmainに反映済みだが、サイトでの反映完了とは区別する。

原因として確認できたこと

articlesの検証・公開ワークフローはセルフホストRunnerで成功した。一方、my-cardの本番とPRプレビューには runs-on: ubuntu-latest が残っていた。本番の直近定期実行も失敗していた。失敗の契約・課金上の理由は、この調査で直接確認した証拠がないため断定しない。

何を変えたか

PR上で2つのワークフローを [self-hosted, Linux, X64] に変更し、ジョブの実行上限を30分に設定した。変更差分はこの実行環境とタイムアウトだけで、アプリコード、秘密情報、権限、定期実行間隔は変更していない。

保持した安全策

本番デプロイの直列実行、デプロイ直前の最新コミット検査、全公開記事と最新ニュースの本番HTML照合を維持した。PRについては、同じリポジトリ由来のPRだけを実行する既存条件を保持した。テストやビルド、HTML一致検証を省略して公開成功扱いにする経路は追加していない。

ハマったこと・学んだこと

Runnerのラベルを指定しただけでは、そのリポジトリで実際にRunnerへ割り当てられることは保証されない。別リポジトリのジョブが動いていても、登録先、アクセス可能な範囲、ラベル、稼働状態、待ち行列を個別に確認する必要がある。queuedでステップがないという観測だけから、未登録とオフラインのどちらかを断定してはいけない。

記事原稿の反映、SNS告知の受付、サイトの再ビルド・デプロイ・公開確認は別の成功条件である。本文50件の生成に成功していても、最終配信先のHTTP 200と内容一致を確認できなければサイト公開は未確認として残す。

未完了事項

my-cardのジョブが対応Runnerに割り当てられ、npm ci/test/buildとプレビューが成功することの確認。その後にPRをマージし、本番デプロイで対象ニュース50件のHTTP 200とHTML一致を確認する。これは未実施の作業項目であり、今回完了したという記録ではない。

毎時の記事取り込み・全件公開確認・CDN誤変換対策のリリース確認

releaseリリース済み

毎時の記事取り込み・全件公開確認・CDN誤変換対策のリリース確認

リリース状況

リリース済み。2026年10月23日16:31:17(日本時間)に、chicchi.workの公開対象59記事と一覧1ページ、計60ページすべてについてHTTP 200とビルドHTMLのSHA-256完全一致を確認した。未一致ページは0件。

my-card PR #16と#18はmasterへマージ済み。最終リリースコミットはd303d4314ed5ef8ddd88649045c0642f429c8530。本番Run 39918214428でテスト、ビルド、最新revision確認、Firebase Hosting本番公開、ニュース確認、全記事確認、検証レポート保存がすべて成功した。

本記録は次の未リリース時点の記録に対するリリース確認であり、過去の検証経緯を消さずに追記する。

  • 2026-10-23-hourly-article-sync-and-full-verification.md
  • 2026-10-23-cdn-code-obfuscation-recovery.md

何を変えたか

  • 記事更新の通知送信元がない環境でも更新を拾えるよう、1日1回だった再ビルドを毎時45分へ変更。
  • 最新1記事だけではなく、ソースでpublished:trueの全記事と一覧を本番照合する機能を導入。
  • Cloudflareがコード例のメール表記やpackage@versionを誤変換する問題を、記事URLへのno-transformヘッダーで解消。
  • 既存の古いビルド公開防止、ニュース公開確認、RSSヘッダーを維持した。

なぜ変えたか

記事保存、サイトへの取り込み、本番配信、公開状態の確認が別工程であることが十分に検証されていなかった。更新通知の受信設定だけでは記事更新を検知できず、最新1件だけの確認では古い記事の配信時変換を見落としていた。

ユーザーにとって何が良くなったか

過去記事を含めて公開本文と一覧を確認できる。Muse記事も実際の本番URLでHTTP 200とHTML一致を確認済み。今後は通知がなくても毎時の処理でソースを取得する。ただしGitHubの定期実行には遅延があり、更新から厳密に1時間以内の公開を保証するものではない。

技術的な内容

検証結果はActions成果物site-article-publication-reportに保存した。

  • checked_at: 2026-10-23T07:31:17.081Z
  • expected_articles: 59
  • expected_pages: 60
  • verified_pages: 60
  • 未確認・失敗ページ: 0
  • Artifact ID: 12485697021
  • ZIP SHA-256: fd56ed39f858cc22c24d2fcfd59c605feb0d132984507b44d9f971e43b0051f6

最初の全件チェックでは60ページ中57件だけ一致し、3記事でCloudflareのメール保護リンクと復号スクリプトの挿入を検出した。検証条件を緩和せず、配信側でCache-Control: public, max-age=0, must-revalidate, no-transformを記事URLに適用し、同じ検査で60件一致を確認した。

判断したこと・トレードオフ

  • 毎時ビルドは即時通知ではない。追加の他リポジトリ書込み権限なしで更新を取り込めることを優先。
  • 記事ページではメール難読化が適用されなくなる。コード例を正確に配信するためであり、サイト全体のWAFや認証は変更しない。
  • 公開確認はページ存在だけでなく本文全体の一致を要求する。HTTP 200のエラーページや古いHTMLを成功にしない。
  • この完了はchicchi.workへの公開に関するもの。Zennの投稿数上限による未公開は別問題として残る。

ハマったこと・学んだこと

公開確認を全件に広げると、最新記事だけでは観測できなかった配信中のコード改変を発見できる。原因の解消と、外部サービス上での最終的な確認結果を別々に記録すると、再発時の切り分けに利用できる。

記事の更新通知がなくても取り込み、過去記事まで本番照合する

reliability未リリース

記事の更新通知がなくても取り込み、過去記事まで本番照合する

リリース状況

恒久対策は未リリース。my-card PR #16で毎時更新と全件検証を準備し、テスト中。 既存の本番公開workflowを再実行し、最新のソースを取り込んだビルド、Firebase公開、最新記事と一覧の本番HTML照合は成功した。恒久対策のマージと全記事照合の結果は別途追記する。

何を変えたか

  • 更新間隔を1日1回から毎時45分へ変更。元の07:45 JSTも含む。
  • 最新記事1件のみだった検証を、ソースでpublished:trueの全記事と一覧へ拡張。
  • ソースから期待する記事集合を求め、ビルド時の生成漏れと一覧への掲載漏れを検出。
  • 本番URLのHTTP 200とビルドHTMLのSHA-256一致を確認する。最大同時3件、未確認だけ最大5回再試行。
  • 各ページの検証結果をJSONで保存し、Actionsの成果物として14日保持。
  • 古い記事の生成漏れ、一覧漏れ、空ソース、古いHTML、soft 404、部分成功、再試行の回帰テストを追加。

なぜ変えたか

サイトは記事リポジトリをビルド時に取得する静的サイトであり、記事をmainへ保存しても公開中HTMLは変わらない。repository_dispatchの受信設定はあったが、記事側に通知送信処理がなく、次の1日1回の定期ビルドまで新規記事が掲載されなかった。

Zennの投稿上限とは別の原因である。QiitaやZennの成功・失敗を、このサイトの公開完了と同一視してはいけない。

ユーザーにとって何が良くなったか

通知がなくても毎時の処理で更新を取り込める。最新1件が表示されるだけでなく、過去の記事も本文と一覧が正しく配信されていることを確認できる。

技術的な内容

共有するTypeScriptのfront matter解析を、Nodeの型注釈除去を有効にして利用する。期待する記事集合を生成済みdistだけから求めると、生成されなかった記事を検出できないため、ソースを基準にする。

HTTP 200だけでは代替エラーページや古いキャッシュを成功と誤認する。各ページのHTMLハッシュをビルド結果と比較し、差異が残る場合は失敗にする。テストではfetchを差し替え、外部サイトへのアクセスや投稿は行わない。

判断したこと・トレードオフ

  • 新しい認証情報や他リポジトリへの書込み権限を追加せず、既存の取得・公開権限で完結する毎時更新を採用。
  • ビルド回数は増える。GitHubの定期実行には遅延があるため、厳密に1時間以内の反映を保証しない。
  • 既存の排他制御と最新revision確認は維持する。古いビルドを公開しない。
  • 本番URLの照合失敗を無視しない。Zennの公開制限をサイト公開のブロッカーにしない。

ハマったこと・学んだこと

更新イベントの受信側だけ実装しても、送信されなければ動作しない。更新検知、ビルド、外部公開、公開確認をそれぞれ別の状態として扱う必要がある。

全件検証は生成済みファイルだけでは不十分。期待するソース集合から照合することで、古い記事の欠落も検知できる。

別リポジトリの記事追加がサイトへ反映されない問題と全件検証

reliabilityリリース済み

別リポジトリの記事追加がサイトへ反映されない問題と全件検証

リリース状況

リリース済み。2026-10-23 16:31 JSTに、公開対象59記事と一覧の計60ページすべてについて、HTTP 200およびビルドHTMLとのSHA-256完全一致を確認した。

実際に採用・マージされた変更はmy-card PR #16(毎時45分の更新と全記事検証)およびPR #18(記事HTMLのCDN変換防止)。本番コミットは d303d4314ed5ef8ddd88649045c0642f429c8530、成功した公開処理は 39918214428、確認レポートの成果物IDは 12485697021。

当初このファイルで記録したPR #17は同じ目的の別実装案だった。PR #16と競合・重複したため未マージでクローズした。#17の毎時17分案と独立したbuild-only検査は採用していない。採用済みの毎時45分設定を上書きしない。このリリース状態は自サイトを指し、Zennの投稿数上限による公開待ちを解消したという意味ではない。

何を変えたか

  • 定期ビルドを1日1回から毎時45分へ変更し、更新通知が届かない場合にも記事ソースを取得する。
  • 記事ソースのpublished:true全件を、生成された詳細ページと一覧へ照合する。
  • 最新1記事だけの確認を、一覧と全公開記事のHTTP 200・HTML SHA-256照合へ変更する。
  • 同時接続3件、未確認のみ最大5回再試行、記事別JSONレポートの保存を追加する。
  • 記事配下だけに Cache-Control: public, max-age=0, must-revalidate, no-transform を設定し、配信経路でコード例を書き換えさせない。
  • 欠落した過去記事、古いHTML、soft 404、部分成功、再試行、配信設定を回帰テストで検証する。

なぜ変えたか

記事正本へのpushとmy-cardのデプロイは独立している。my-cardにはrepository_dispatchの受信設定はあったが、記事リポジトリのpushだけで通知される構成ではなく、1日1回のビルド後に追加された記事は取り込まれていなかった。これはZennの投稿数上限とは別問題である。

全件確認の導入後、59記事と一覧の60ページ中57ページのみ一致し、3ページのHTML不一致も発見した。HTTP 404ではなく、CloudflareのEmail Address Obfuscationによる変換が原因だった。

ユーザーにとって何が良くなったか

更新通知がなくても毎時取得によって新着を反映し、過去記事の掲載漏れや配信内容の改変も検出できる。記事本文・URL・Qiita/Buffer投稿IDは変更しない。

技術的な内容

GitHub Actionsでアプリのmasterと記事のmainを取得し、既存のテスト、静的ビルド、本番競合防止ガードを維持したまま公開する。照合対象はdistだけでなくソースから作るため、生成時に黙って省かれた記事が検査対象からも漏れることを防ぐ。

公開後は正規URLへリダイレクト禁止でアクセスし、HTTP 200に加えて生成HTMLと取得HTMLのSHA-256一致を要求する。404ページが200で返る場合や古いHTMLを成功にしない。結果は site-article-publication-report.json として14日保持する。

読み取り専用の追加診断でも、DynamoDBの記事のサンプルメール、Codex記事の [email protected]、OIDC記事の [email protected] などが、Cloudflareのemail-protectionリンクへ置換されることを確認した。Cloudflare公式が案内するno-transformを記事URLだけに付与し、完全一致検査は緩和せずに全件成功させた。

判断したこと・トレードオフ

  • 新しい他リポジトリ用認証情報を仮定せず、既存の読み取り権限と公開処理で動く毎時ビルドを採用した。Actions実行時間は増加する。
  • 定期実行には遅延があるため「必ず1時間以内」とは保証しない。即時通知の送信側は別途整備が必要。
  • ビルド中に正本が更新された場合に古い内容で上書きしない既存チェックを維持する。
  • HTMLの不一致を無視して成功にせず、配信側の変換原因を修正する。
  • no-transformは記事URLに限定し、サイト全体のWAFやメール保護を無効化しない。記事には公開可能なサンプルのみ記載する。
  • max-age=0とmust-revalidateにより再検証通信は増え得るが、技術記事の忠実な配信を優先する。

ハマったこと・学んだこと

  • 外部公開先への投稿と自サイトへの取り込みは異なる成功条件を持つ。
  • 最新記事1件の成功は過去記事すべての存在を保証しない。
  • 生成物だけを列挙する検査では、生成に失敗した項目自体を見落とす。正本から期待する全件集合を作る必要がある。
  • CDNのメール保護は、npmパッケージやGitHub Actionsのバージョン記法まで誤認する場合がある。
  • 同時に進行した実装は最新masterと比較して統合し、競合した古い案を重ねてマージしない。

全件公開チェックで検出したCloudflareのコード例誤変換

bugfix未リリース

全件公開チェックで検出したCloudflareのコード例誤変換

リリース状況

この追加対策は未リリース。PR #18で検証中。先行するPR #16の毎時更新と全記事確認はmasterへマージし本番実行済みだが、全記事のHTML一致はまだ完了していない。本番照合が完了したら別のリリース確認記録へ追記する。

何を変えたか

  • 全公開記事59件と一覧の計60ページを照合し、57件が完全一致、残る3記事はHTTP 200だがHTML不一致であることを検出。
  • 3記事の配信HTMLとビルド結果を読み取り専用で比較し、Cloudflare Email Address Obfuscationによる置換を特定。
  • Firebase Hostingの/articles/**だけにCache-Control: public, max-age=0, must-revalidate, no-transformを設定。
  • 対象範囲の限定、RSSヘッダー維持、コードの置換を検証が拒否することの3テストを追加。
  • 一時診断workflowを削除し、本番ブランチには取り込まない。

なぜ変えたか

配信途中でサンプルメールに加えてnpmのパッケージ指定やGitHub Actionsのuses:値がメールと誤認され、/cdn-cgi/l/email-protectionへのリンクと復号スクリプトへ置換されていた。記事の存在確認だけでは見えなかった問題を、全HTMLの一致チェックで検知した。

ユーザーにとって何が良くなったか

技術記事のコマンドやコードを、JavaScriptによる復元に依存せず原稿どおりに配信できるようにする。正しくないHTMLを検証側で無視して成功にする変更は行わない。

技術的な内容

問題はDynamoDB読み取り整合性記事、Codex Day 4記事、GitHub Actions OIDC記事で観測した。診断では3件すべてにCloudflareのメール保護リンクと復号スクリプトが追加されていた。

Cloudflare公式はCache-Control: no-transformがあるページではメール難読化を適用しないと説明している。これを記事URLに限定して設定する。max-age=0, must-revalidateによりキャッシュ利用時の再検証も要求する。

判断したこと・トレードオフ

  • 検証側でCloudflareの挿入要素を取り除いて比較する方法は採用しない。変換されたコードを見逃すため。
  • 全サイトのメール保護、WAF、認証、アクセス制限は変更しない。
  • 記事URLではメール難読化されなくなるため、原稿には公開可能な例だけを記載する。
  • キャッシュ再検証の通信は増え得るが、技術記事の忠実な配信を優先する。
  • 記事本文、タイトル、slug、Qiita/Buffer投稿IDには変更を加えない。

ハマったこと・学んだこと

ビルドとデプロイが成功していても、CDNなどの中継処理で配信HTMLが変わる場合がある。メール保護はメールアドレスだけを対象にするとは限らず、name@versionのような技術的な文字列にも影響し得る。

最新記事だけではなく全件のHTMLを照合することで、以前から存在していたコード例の誤変換も検出できた。

静的サイトの記事未反映を再デプロイし、公開URLを自動検証

bugfixリリース済み

静的サイトの記事未反映を再デプロイし、公開URLを自動検証

リリース状況

リリース済み(2026-10-22)。

my-card PR #15 をマージし、本番Firebase Hostingへのデプロイが成功した。デプロイ後に最新の記事の一覧・詳細ページでHTTP 200とビルドHTMLのSHA-256一致を確認した。

何を変えたか

新しく公開した記事がchicchi.workに存在しない問題を修正するため、最新の articles/main を取り込んで本番サイトを再ビルド・デプロイした。

さらに scripts/verify-published-articles.mjs を作成し、Firebase Hostingの本番デプロイ後に、日付付き最新記事の一覧・詳細ページについて、HTTP 200とビルドされたHTMLとのSHA-256ハッシュ一致を検証する処理を追加した。デプロイ直後の反映遅延を想定して最大5回再試行し、結果が一致しなければGitHub Actionsを失敗にする。

tests/deploy-workflow.test.ts に、公開検証処理がデプロイ後に実行されることを確かめる回帰テストを追加した。

なぜ変えたか

articles リポジトリへの記事追加はQiita/Zennへの公開を起動するが、my-card はAstroのビルド時に別リポジトリのMarkdownを取得して静的HTMLを生成する。

2026-10-22にPHP2Shellの記事を公開した時点で、同日朝の定期ビルドはすでに完了しており、新しい記事の静的HTMLが本番に存在しなかった。記事側が published: true でも、公開済みページを生成したことにはならない。

ユーザーにとって何が良くなったか

  • PHP2Shellの記事が /articles/2026-10-22-php2shell-cve-2026-rce-explained/ から閲覧できるようになった。
  • 記事一覧と詳細ページの本番反映漏れをデプロイ完了時に検出できるようになった。
  • 既存の日次ニュース公開確認に加え、通常記事についてもデプロイ後の動作確認が自動化された。

技術的な内容

articles/main にMarkdownを追加
         ↓
my-card の再ビルド契機(master push / 定期実行 / 更新通知 / 手動)
         ↓
最新の記事リポジトリをcheckout
         ↓
npm test → npm run build(Astro静的生成)
         ↓
Firebase Hostingへデプロイ
         ↓
記事一覧+最新詳細ページをHTTPS取得
         ↓
HTTP 200 / ビルドHTMLとSHA-256一致を検証

dist/articles に存在する日付付きの最新記事ディレクトリを見つけ、対応する index.html が記事一覧からリンクされているかを確認した後、実際の本番ページとビルド済みHTMLのハッシュを比較する。検証URLには Cache-Control: no-cache を付与し、通信タイムアウトと再試行を設けている。

Before / After

Before

  • 定期ビルドの後で公開された記事は次回のmy-cardデプロイまで404。
  • GitHub Actionsのデプロイ成功は、記事の本番反映の成功を保証していなかった。

After

  • 最新記事を取り込んで再ビルド・再デプロイしたことで対象記事の詳細URLが公開された。
  • デプロイ後に記事一覧・個別記事を本番から読み直す確認が行われる。

判断したこと・トレードオフ

  • 本番環境での確認をCIの最後に配置した。静的ビルド成功だけでは独立した配信環境での公開漏れが検出できないため。
  • HTML完全一致で検証した。HTTP 200だけでは、古いコンテンツや誤った記事が返されても検出できない。
  • 一時的なキャッシュ反映の遅延で即座に失敗しないよう再試行を実装した。
  • 今回の変更だけでは、記事リポジトリのpushからmy-cardのデプロイを即時起動する仕組みは追加していない。 現在の再ビルド契機はmaster push、定期実行、別途送信される更新通知、手動実行であり、この制約は残る。

ハマったこと・学んだこと

  • コンテンツのGit管理上の公開と、静的ホスティングの実際の反映は別の段階。別リポジトリ管理ではとくに反映タイミングがずれやすい。
  • GitHub Actionsが成功しても、公開URLが存在しない可能性がある。エンドツーエンドの公開検証を導入することが重要。
  • ハッシュ検証はHTML完全一致を要求するため、CDNやホスティング側でHTMLを変更する構成では検証方式を調整する必要がある。

静的ニュースの公開漏れを復旧し、本番HTMLの照合を公開処理に追加

reliabilityリリース済み

静的ニュースの公開漏れを復旧し、本番HTMLの照合を公開処理に追加

リリース状況

リリース済み。2026年10月16日23:14:25(Asia/Tokyo)に、GitHub Actionsから本番ドメインのニュース一覧と2026年10月15日分の詳細ページを取得し、両方がHTTP 200で応答し、生成したHTMLと一致することを確認した。詳細ページの番号付きニュース見出しは50件だった。

今回の修正コミットは my-card の 1d7a3d106d775a20d9eca0ad22dc43ac2a196a34。本番デプロイと新しい公開確認ステップは、Actions実行 38914060609 で成功した。

何を変えたか

ニュース原稿の追加後に本番サイトの再ビルドが実行されていなかったため、既存のデプロイ処理を再実行して公開を復旧した。続いて .github/workflows/firebase-hosting-merge.yml に、本番URLの内容を検証する Verify published news on the production domain ステップを追加した。

Firebaseへのアップロード成功だけでなく、利用者がアクセスするニュース一覧と最新の詳細ページを取得し、今回のビルドで生成したHTMLが配信されているか確認する。

なぜ変えたか

コンテンツを管理するリポジトリと、Astroでサイトを生成するリポジトリが分かれている。原稿がコンテンツ側の本番ブランチに保存され、形式検証に成功しても、公開サイトの再ビルドと配信が完了しているとは限らない。

今回、サイト側の定期デプロイより後にニュース原稿が追加されたため、サイトにはその内容を含まない古い生成結果が残っていた。GitHubへの保存、原稿の検証、サイトへの配信を別の到達点として扱う必要があった。

ユーザーにとって何が良くなったか

10月15日分の50件版がニュース一覧から参照できるようになった。また、デプロイ処理が実行された際、一覧だけ古い状態や、詳細ページが欠ける状態、以前の少ない件数の原稿が配信される状態を検出できるようになった。

技術的な内容

  1. dist/news/ から日付形式のディレクトリを抽出し、最新のニュース日付を決める。
  2. 生成済みの一覧HTMLにその日の詳細ページへのリンクがあることと、詳細の番号付き見出しが1〜100件であることを確認する。
  3. 本番ドメインの /news/ と /news/<date>/ を実際に取得する。確認専用のクエリ文字列は付けず、通常のURLを検証する。
  4. HTTP 200であることに加え、応答HTML全体のSHA-256と生成済みHTMLのSHA-256を比較する。SHA-256は内容の一致確認に用いるハッシュ値であり、内容が変わると値も変わる。
  5. 取得処理は20秒で打ち切り、失敗時は5秒間隔で最大5回試す。確認できなければステップを失敗させ、未確認の状態を成功扱いにしない。

Node.js標準機能だけを利用し、新しい外部パッケージや認証情報は追加していない。既存の本番デプロイの直列化、古いリビジョンの公開防止、定期・手動・通知・pushの起動条件は維持した。確認失敗時にも公開済みコンテンツの自動削除や巻き戻しは行わない。

Before / After

Before

原稿の保存と検証は成功していたが、ニュース一覧の最新日は10月13日のままだった。デプロイ成功後の本番ページ内容を検証するステップもなかった。

After

10月15日の50件版を含むサイトを公開した。本番の一覧と詳細が今回生成したHTMLに一致することを、デプロイ後の独立したステップで確認した。

検証結果

  • 既存のサイトテスト: 70件成功、失敗0件。
  • Astroの検査: エラー0件、警告0件。静的ビルドとFirebaseの本番配信が成功。
  • 追加処理のローカル検証: 6ケース成功。正常な50件版、古い20件版、詳細404、古い一覧、不正な生成済み件数、ニュース未生成の各ケースで期待した成功・失敗を確認した。この6ケースは既存の70件とは別に実施したローカル検証である。
  • 本番公開確認: 一覧と10月15日分の詳細がともにHTTP 200。HTMLのハッシュが生成結果と一致し、詳細は50件。

公開確認ログに記録されたハッシュ:

/news/
5534730a04f35d2657855a7c33a2d194c0d2e5ad41de405b8bf7f767ab883e76

/news/2026-10-15/
6ad69dc4872216b189f6af8415a70d167653c33ea0aadc8cf3e92e52f5f9f07b

判断したこと・トレードオフ

ページの存在や見出しの件数だけでは、同じ件数の古い原稿を見逃す可能性があるため、HTML全体を照合した。現在の静的配信には適合するが、将来CDNなどでHTMLを書き換える構成に変更した場合は比較方法の再検討が必要になる。

最新ニュースの一覧・詳細を対象とする確認であり、過去の全ページの公開確認ではない。また、HTMLが一致することは配信内容の確認であり、ニュース本文の事実確認の代わりにはならない。

ハマったこと・学んだこと

  • コンテンツのコミットと静的サイトの公開は別処理であり、それぞれ成功を確認する必要がある。
  • デプロイサービスの成功応答だけでは、本番ドメインから期待した内容を取得できるかは分からない。
  • 一覧と詳細は両方を確認する。一覧だけ新しい場合も、詳細だけ存在する場合も、公開完了とは扱わない。
  • 原稿リポジトリの更新中にビルドすると既存のリビジョン確認で公開がスキップされるため、公開処理の実行結果と確認ステップが実行されたかを区別する。

存在しない日付をRSS・sitemap・構造化データへ出さない共通検証

bugfix未リリース

存在しない日付をRSS・sitemap・構造化データへ出さない共通検証

リリース状況

未リリース。修正を実装してローカル検証済み。本番反映前。

この記録は実装と検証の到達点を示す。本番での提供を確認したリリース記録ではない。対象は my-card Issue #7。修正は PR #12 として提出済みで、この記録時点では未マージ。

何を変えたか

記事、日次ニュース、プロダクト更新で使用する日付の検証を共通化した。YYYY-MM-DD の形に加えて、その年月日が実在することを確認する。

入力例 修正後の判定
2026-02-29 拒否。2026年は閏年ではない
2026-13-01 拒否。13月は存在しない
2026-04-31 拒否。4月31日は存在しない
2026-02-31 拒否。別の日付へ自動補正しない
2028-02-29 許可。実在する閏日

検証を通った日付を、RSSの公開日時、sitemapの更新日、JSON-LDとOGメタデータの公開・更新日時に使う。RSSは更新情報の配信形式、sitemapは公開URLなどを検索エンジンへ伝えるファイル、JSON-LDは記事の種類や日付を機械が読み取れる形式で記述するデータ。

なぜ変えたか

従来の処理は日付文字列の先頭が正しい形かどうかを主に確認していた。実在しない日付がそのまま出力される場合と、JavaScriptのDateへの変換で別の日付に補正される場合があり、入力の誤りを検出できなかった。

さらに、Markdown先頭のメタデータであるfront matterをYAMLとして読む段階にも原因があった。引用なしの date: 2026-02-31 は、既定の解析設定では検証前にDateへ変換・補正され得る。変換後のDateだけを調べても、元の入力が「2月31日」だったことは分からない。

ユーザーにとって何が良くなったか

RSSリーダーや検索エンジンへ、存在しない日付や意図せず補正された日付が渡るのを防ぐ。公開日が分からない記事に架空の日付を表示することも避ける。

有効な記事は引き続き掲載できる。日付を使える根拠がある場合はその値を使い、根拠がなければ日付を省略する。検索順位の改善などの効果は測定しておらず、この記録では断定しない。

技術的な内容

元の入力を残すYAML解析

src/content/frontmatter.ts に共通の parseFrontmatter を追加し、gray-matter のYAML解析処理に js-yaml の JSON_SCHEMA を指定した。これはYAMLを読む際にどの値を文字列・真偽値・数値などへ解釈するかを決める設定。

日付の自動Date変換を使わず、日付を文字列のまま検証へ渡す。published: true の真偽値や item_count: 12 の数値は維持する。記事、ニュース、プロダクト更新の各ローダーで、この共通解析を使用する。

UTCで年月日を再構築する

src/content/calendar-date.ts で、文字列の長さと形式を確認して年・月・日を分解する。次に setUTCFullYear(year, month - 1, day) でUTCの日付を再構築し、取り出した年月日が入力と一致する場合だけ許可する。UTCは地域ごとの時差に左右されない基準時刻。

この方法なら、Dateが月を繰り上げてしまった場合に入力との差を検出できる。年・月・日の確認はサーバーのローカルタイムゾーンに依存しない。

日付と日時の受け付け方を明確化する

YYYY-MM-DD だけの有効な日付は受け付ける。時刻を持つ文字列では、T 区切りの時刻と、Z または +09:00 などの明示的なタイムゾーンを必要とする。日付だけでなく、時・分・秒と時差の範囲も確認する。

例えば 2028-03-01T00:30:00+09:00 のカレンダー日付は 2028-03-01 のまま保持する。RSSの日時へ変換するときは、同じ瞬間をUTC表記で出力する。日付の取り出しと、時刻の表示形式の変換を区別した。

無効値の扱いを出力先でそろえる

対象 無効値・欠損値の扱い
記事・プロダクト更新の日付 有効なfront matterの日付、有効なファイル名の日付の順で採用し、両方使えなければ null
ニュースの日付 ファイル名と実在日付の一致を要求し、不正なニュースを読み込み対象から除外
ニュースの生成・更新日時 不正値を null にし、生成日時がなければニュースの日付の07:00 JST、更新日時がなければ公開日時を使用
記事RSS 日付不明の記事は掲載を維持し、pubDate を省略
sitemap 有効な更新日だけを lastmod へ出力
JSON-LD・OGメタデータ 共通の日付選定結果を使用し、根拠のない日時フィールドを省略

src/content/publication-dates.ts に公開・更新日時の選定をまとめ、ページとRSSから利用する。ニュースRSSのXML生成も buildNewsRss として切り出し、生成結果を直接テストできるようにした。

Before / After

Before

形式が日付に見えれば実在しない日付も通過していた。YAML解析が先に値を補正する経路もあり、後からの検証では元の入力を判断できなかった。記事RSSは日付がなければ1970年を補っていた。

After

YAML解析で元の日付文字列を保持し、年月日の実在性を確認してから出力先へ渡す。RSS、sitemap、構造化データは共通の検証を使用する。日付が不明な記事には架空の日付を補わない。

判断したこと・トレードオフ

  • 出力直前の検証だけでなく、解析段階の自動補正も止めた。入力の誤りが途中で消えると、正しい検証関数を追加しても検出できないため。
  • 記事とプロダクト更新では、日付が不正でも本文を一律に非公開にせず、有効なファイル名の日付へフォールバックする。どちらにも根拠がなければ日付なしとして扱う。
  • ニュースは日付がURLの一部になるため、不正な日付のファイル自体を除外する。記事の日付と同じ欠損処理にはしない。
  • 日時の書式は明示的に限定する。タイムゾーンのない文字列を環境任せで解釈せず、無効なメタデータとして扱う。
  • RSSの pubDate は任意要素なので、日付不明時は省略する。架空の1970年を与えて情報を埋める方法は採用しない。RSS 2.0仕様で、itemの要素はtitleまたはdescriptionの要件を除き省略できることを確認した。

ハマったこと・学んだこと

正規表現だけ直してもYAMLの自動補正は止まらない

今回の主要な知見は、検証を行う時点で元の値が残っているかを調べる必要があること。2026-02-31 という文字列のテストだけでなく、引用あり・引用なしのfront matterをローダーへ渡す回帰テストを追加した。

Date.UTCには0〜99年の扱いに注意が必要

Date.UTC は0〜99の年を1900年からの年数として扱うため、4桁の年をそのまま検証する目的では注意が必要だった。今回の再構築には setUTCFullYear を使い、0096-02-29 も確認した。これは本サイトの入力検証の選択であり、日付を表すすべての用途で同じ方法が必要という意味ではない。ECMAScriptのDate.UTC仕様に年の変換規則がある。

型ガードはfalse側の型も変える

最初は isCalendarDate(value): value is string と宣言していた。しかしこの関数が判定したいのは、文字列かどうかに加えて実在する日付かどうか。すでに string と分かっている値にこの型ガードを使うと、false側が「stringではない」と解釈され、never になる型エラーが発生した。never は、その場所に取り得る値がないことを表すTypeScriptの型。

検証済みの文字列だけを表す CalendarDate 型を追加し、型ガードの戻り値をその型へ変更した。文字列に型の識別情報を付けるbranded typeを使っている。

type CalendarDate = string & {readonly __calendarDate: unique symbol};

これで、成功時は検証済み日付、失敗時は通常の文字列として後続の解析を続けられる。型ガードの判定はtrue側だけでなくfalse側にも影響するため、実行時テストに加えて型チェックも必要だった。TypeScript Handbookの型述語も両分岐の絞り込みを説明している。

検証結果

日付対応時のローカル検証では、以下を確認した。件数はその時点のコンテンツとテストに対する値であり、固定のサイト仕様や性能測定値ではない。

検証 確認結果
日付関連8ファイルのテスト 34件すべて成功
依存関係を再インストールした後の全体テスト 61件すべて成功、失敗0件、スキップ0件
最終的なAstroの型チェック エラー0件、警告0件、ヒント0件
実コンテンツを含む静的ビルド 61ページ生成成功
生成済みRSSの公開日時 46件すべて解析可能
生成済みsitemapの更新日 50件すべて実在日付
生成済みHTML内のJSON-LD日時 92件すべて解析可能

回帰テストには、閏日、不正な月末、未引用YAML日付、ファイル名との不一致、無効な時刻・時差、ファイル名へのフォールバック、RSSからの不正日時除外を含めた。これらの成功だけで本番反映済みとは判断していない。

更新履歴の公開可否とリリース状態を分けて扱う

bugfix未リリース

更新履歴の公開可否とリリース状態を分けて扱う

リリース状況

未リリース。PR #12を作成済みで、masterには未マージ。修正を実装し、実際のコンテンツを使ったローダーのテストと契約の照合を実施した。本番反映は未確認。

何を変えたか

更新履歴を読み込む ProductUpdate 型に status を追加し、元の値を保持する。更新一覧には「開発中(未リリース)」「未リリース」「リリース済み」を表示する。

記録の値 公開画面の表示
development 開発中(未リリース)
unreleased 未リリース
released リリース済み
shipped リリース済み

なぜ変えたか

記事の正本にはstatusがあるのに、表示側のローダーと型がその情報を捨てていた。publishable: true は内容を公開してよいという許可であり、本番で利用可能という意味ではない。これを混同すると、未リリースの変更を提供済みと誤解させてしまう。

ユーザーにとって何が良くなったか

更新履歴が開発中の記録なのか、すでに本番で提供されている変更なのかを区別できる。公開してよい開発中の情報も引き続き確認できる。

技術的な内容

statusには4つの文字列だけを許可するunion型を使用する。union型は、列挙した候補のいずれかだけを受け付けるTypeScriptの型である。欠落、未知の値、文字列以外の値は警告して該当更新を除外し、勝手にリリース済みへ補正しない。

shipped は既存記録との互換のためそのまま保持し、表示上は released と同じ意味とする。公開フラグがtrueなら未リリースも表示し、falseなら状態に関係なく除外する。

CIではビルドに使うのと同じarticlesの取得先から products/update-metadata.json を読み、表示側の値域、リリース判定、別名の意味を照合する。記事を取得していないローカル環境ではこの照合のみ明示的にスキップするが、取得済みなのに定義が欠落・不正ならテストを失敗させる。

判断したこと・ハマったこと

別リポジトリに正本があると、型の候補をコピーしただけでは将来の変更に追従できない。CIで実データと同じ版の定義を比較し、変更の見落としを検出することにした。

元データの別名を一括で正規化すると過去の記録の意味を追いにくくなるため、保持する値と利用者に見せるラベルを分けた。

検証結果

ローダーとstatusの10テストがすべて成功し、契約照合もスキップなしで成功した。確認時点の実データでは5プロダクト・107更新を警告なしで読み込んだ。開発中79、未リリース18、released4、shipped6の全状態を保持した。

日次ニュースの生成後にサイトを再ビルドする

ci未リリース

日次ニュースの生成後にサイトを再ビルドする

リリース状況

未リリース。PR #12を作成済みで、masterには未マージ。workflowとREADMEの修正、設定を確認する回帰テストを実装した。定期実行と更新通知の起動設定は、masterへのマージ後に有効になる。実際のニュース生成完了や本番デプロイまでをこの検証で実行したものではない。

何を変えたか

日次の再ビルドを 45 22 * * * に変更した。22:45 UTCは翌日の7:45 JSTに相当し、articlesのREADMEにある7:00 JSTのニュース生成予定より後になる。

更新通知用の repository_dispatch も受け付け、articles-updated または news-updated の通知が来た際に再ビルドする。記事はarticlesのmainブランチから取得する。

なぜ変えたか

以前の 45 21 * * * は6:45 JSTで、README記載のニュース生成より前だった。READMEが約束する再ビルド時刻や更新通知の仕様と、実際のworkflowの起動条件も一致していなかった。

ユーザーにとって何が良くなったか

ニュースを生成する前にサイトをビルドしてしまう時刻の逆転を解消する。更新通知を受けた場合も、最新のコンテンツを取り込む同じ処理が使える。

技術的な内容

schedule は時刻による起動、repository_dispatch は外部からAPIで送る通知による起動である。どちらも同じjobへ接続し、コンテンツ取得、依存導入、テスト、ビルド、本番デプロイの順に実行する。取得やテストに失敗したまま公開を継続しない。

設定の回帰テストではYAMLを解析し、cronを日本時間の分単位に変換して7:45であることを確認する。通知の種類、masterへのpushと手動実行、記事取得のmain指定、テストとビルドがデプロイより前にあることも検証する。

判断したこと・ハマったこと

コメントに日本時間を書くだけでは、cronと説明がずれた際に検出できない。設定の実値を解析して期待する日本時間と比較することで再発を防ぐ。

定期実行は外部のニュース生成の終了を直接待つ仕組みではない。また、GitHub Actionsの混雑で開始が遅れる可能性もある。7:45は開始の目安として説明し、更新通知の受信と定期実行を併用する。

この変更は通知の受信側である。articlesへのpushだけで通知が送信される仕組みを新設したとは説明せず、READMEに送信側が別途必要なことを記載した。通知がなくても定期実行と手動実行で最新データを取り込める。

検証結果

workflowの回帰テスト3件が成功した。独立レビューでも、時差換算、READMEとの一致、通知設定、記事取得から公開までの実行順に追加指摘はなかった。

Markdown本文とJSON-LDの出力を別々に保護する

security未リリース

Markdown本文とJSON-LDの出力を別々に保護する

リリース状況

未リリース。PR #12を作成済みで、masterには未マージ。修正を実装し、ローカルの回帰テストと独立レビューを実施した。本番反映前であり、PRのマージや本番デプロイの完了を意味しない。

何を変えたか

記事、日次ニュース、プロダクト更新が共通で使うMarkdown描画関数にHTMLのサニタイズを追加した。サニタイズは、表示に必要な要素や属性だけを残し、スクリプト実行につながる内容を取り除く処理である。

加えて、ページの検索エンジン向けデータであるJSON-LDには専用のシリアライズ関数を用意した。JSON文字列内の < を \u003c に置き換えてからHTMLのscript要素へ埋め込む。

なぜ変えたか

MarkedはMarkdownをHTMLへ変換するが、変換結果の安全性は保証しない。本文をそのまま set:html へ渡すと、HTMLのイベント属性や危険なURLが出力される可能性があった。

本文の対策後に出力経路を追うと、タイトルなどがJSON-LDへ渡される別の経路も見つかった。JSON.stringify はJSONとして正しい文字列を作る処理であり、HTML内のscript要素を安全にする処理ではない。

ユーザーにとって何が良くなったか

Markdownやメタデータに不適切なHTMLが混ざっても、ページ上で意図しないスクリプトが動く経路を防ぐ。通常のコード、表、リンク、画像、タスクリストの表示は維持する。

技術的な内容

  • renderMarkdown で、Marked変換後のHTMLを sanitize-html に渡す。
  • HTMLの要素、属性、URLの方式を許可リストで制限する。
  • コードの language-*、表の配置、番号付きリストの開始番号を維持する。タスクリストの入力要素は必ず操作できないチェックボックスとする。
  • serializeStructuredData でJSONの < をエスケープし、共通レイアウトのJSON-LD出力に適用する。
  • JSONを読み戻した結果が元のデータと一致することも検証し、日本語、絵文字、引用符、改行、既存のバックスラッシュを壊さない。

判断したこと・ハマったこと

出力先の文脈によって必要な対策が異なる。HTML本文にはHTML用のサニタイズが必要であり、JSON-LDにはJSONの内容を保ちながらHTMLの解析状態を変えないエスケープが必要だった。

HTMLの解析器はJSONの引用符の中でもscript終了タグを認識する。したがって本文だけのサニタイズではタイトル経由の問題が残る。JavaScript文字列で使える \x3C はJSONでは無効なので、JSONとして有効な \u003c を選んだ。

正規表現だけでHTMLを除去する方式は採用せず、HTML構文を扱う既存ライブラリを使用した。画像や表などの通常表示が壊れないよう、必要な許可範囲を回帰テストで明示した。

検証結果

Markdown関連6件とJSON-LD関連3件のテストが成功した。独立レビューでは追加の攻撃入力11ケースを検査し、JSON-LDの6ケースではHTML解析後に追加の実行可能scriptが生まれず、後続の本文も維持されることを確認した。

既存153本文の変換前後を比較し、コード1179個、表18個、タスクリスト8個で内容や必要な属性の回帰を認めなかった。実データに画像はなかったため、画像はテスト用データで確認した。

PointPia(Game Score Manager)

複数人でゲームのスコアとラウンド履歴を記録・共有するWebアプリ。通常の点数管理に加え、麻雀やポーカーの記録を扱う。

公式ページ

npm auditでESLint間接依存のminimatchが検出されたときの限定対処

security未リリース

npm auditでESLint間接依存のminimatchが検出されたときの限定対処

リリース状況

未リリース(本番反映未確認)。 2026年11月4日、PointPiaのPR #82がmasterへマージされた。変更を含むCIの品質チェックは通過したが、この記事の記録時点ではFirebase App Hostingへの反映が完了したことを確認できていない。品質ゲートの通過と本番デプロイの成功は別の判定として扱う。

何を変えたか

  • @typescript-eslint/typescript-estree が内部で利用する minimatch を、lockfileのoverridesで11.0.1へ解決するよう変更した。
  • npm auditを読む独自の依存関係監査スクリプトに、[email protected]に関する条件を限定した一時例外を追加した。
  • 例外は開発依存だけ、所定のバージョン・脆弱性URL・深刻度・依存経路に限定し、2026年11月18日を超えたら拒否する。
  • ESLint関連で間接依存・peer dependencyが循環するケースでも判定できるよう、訪問済み集合を使用する再帰探索と回帰テストを加えた。

なぜ変えたか

2026年10月31日から11月3日のPointPiaニュース記事はGitHubへ保存できた一方、開発依存を含むnpm auditでGHSA-g3hv-75wg-m249に由来する警告が検出され、Quality Gateが失敗してFirebaseデプロイが停止していた。アプリ本体の新機能と無関係なESLintツールチェーンの間接依存でも、リリース工程全体が止まる点が課題だった。

ユーザーにとって何が良くなったか

監査を丸ごと無効にせず、既知の開発依存に限定した例外と互換バージョンへの更新で、記事などの安全な変更を再びビルド・デプロイ工程に進められるようにする。実際の公開完了は本番URLでの一覧・詳細ページ・本文一致の検証が別途必要で、PRマージだけでは確認できない。

技術的な内容・Before / After

Before: [email protected]および[email protected]などの検出がESLintの間接依存へ伝播し、npm run audit:dependenciesが失敗した。過去の例外判定はbracesに特化しており、脆弱性グラフの複数ノード・循環経路を扱えなかった。

After: ネストされたminimatchを11.0.1へ更新。古い3.1.5は、アドバイザリが挙げるoptimizationLevel >= 2の事前処理を持たないという調査結果を根拠に、対象経路が開発依存に閉じている場合に限り期限付きでレビュー例外を認める。想定外のノード、異なるバージョン、更新期限切れでは監査失敗のままとする。

判断したこと・トレードオフ

  1. npm audit自体を停止したり、すべてのmoderate警告を無視する方針は採らなかった。
  2. 間接依存を機械的に全面更新するとESLint系の互換性を壊すリスクがあるため、更新可能な箇所は先に固定し、残る経路は評価条件・再確認期限を明文化した。
  3. 期限付き例外は恒久的な修正ではない。上流パッケージの対応状況を再確認し、安全な版へ移行するまでの運用上の負債として追跡する。

ハマったこと・学んだこと

  • npm auditの「14件」は独立した14個の脆弱性とは限らず、同じ脆弱性が複数の依存元へ伝播した警告を含む。見かけの件数より原因パッケージと到達経路を見る。
  • 依存グラフは必ずしも木構造ではない。ESLint/TypeScript ESLintの循環を考慮しない再帰判定は誤判定や無限探索につながる。
  • 開発依存の例外を設ける場合でも、dev: true、固定バージョン、アドバイザリURL、severity、依存チェーン、期限のすべてを検査し、知らない検出結果を通さないことが重要。

記事化の切り口

「npm auditが同じ警告を14件出す理由:ESLintの依存グラフと期限付きレビュー例外の設計」

PointPiaの見た目を保ち、スマホ・タブレット・PCの幅へ対応

uxリリース済み

PointPiaの見た目を保ち、スマホ・タブレット・PCの幅へ対応

リリース状況

リリース済み。PR #75をmasterへマージし、App Hostingの公開ビルドREADY・実トラフィック100%・ソースコミット33eeba17e768c4ef907c7706ed4f9cf177219a96の一致を確認した。親が本番の端末幅ごとの表示と、テスト用ゲームの作成・記録・麻雀操作を確認した。

何を変えたか

ピンクのヘッダー、丸いカード、Zen Maru Gothic、既存ボタンを維持し、画面幅に応じて横幅と配置を切り替えた。ホームはPCで作成フォームと使い方を横並びにし、ゲーム画面では入力・操作とスコア履歴を横並びにした。タブレットではモード選択やカードを2列にし、ニュース一覧はPCで3列にした。会員のゲーム履歴も広い画面で2列になる。

なぜ変えたか

ホームの外枠が上限448px、通常ゲーム画面が上限576pxに制限され、PCでもスマホ向けの縦長配置になっていた。ユーザーはデザインの刷新を望まず、各端末の画面幅を活用したいと希望した。

ユーザーにとって何が良くなったか

PCでは操作欄と結果を同時に確認しやすくなり、説明や履歴を読むための縦スクロールを減らせる。スマホは既存の1列の操作順を維持する。ログインとダイアログは横に広げすぎず、記事本文も読みやすい幅に制限した。

技術的な内容

Tailwind CSSのmd(768px)、lg(1024px)、xl(1280px)で最大幅とgrid列数を切り替える。PCの最大幅は1152px。入力と履歴を別要素で囲み、DOMの操作順、API、権限判定を維持した。min-w-0と文字列の折り返しにより、狭い入力欄や長いゲーム名のはみ出しに対処した。大会の会場表示は既存の広い1列表示を維持する。麻雀だけは卓の絶対配置カードと中央操作が重ならないよう、1280px以上で2列にする。

Before / After

変更前は1440px画面でもホームのフォームが448pxだった。変更後は外枠1152px、作成フォーム560px、説明領域560pxと32pxの間隔になる。375pxではフォーム343pxの1列を維持する。通常ゲームは1440pxで552pxの2列になり、375pxでは343pxの1列になる。

判断したこと・トレードオフ

新しいテーマや端末判定のJavaScriptは導入せず、既存の装飾と画面幅に基づくCSSを使った。PCでページ全体を単純に引き伸ばすと読みづらいため、フォームと説明、操作と履歴を並べる構成にした。ログインの上限512pxは維持する。

ハマったこと・学んだこと

  • GTRでnode_modulesを自動コピーしても最新masterのロックと一致するとは限らない。npm ci後のバージョンを確認し、Nodeも.nvmrcの24.22.1へそろえた。
  • 標準Node26ではlocalStorage依存の基準テストが失敗した。ソースを変更せずNode24へそろえると全1211テストが通った。
  • ゲーム画面のCSPはunsafe-evalを許可しないため、next devのReact Refreshが停止した。CSPを緩めず、本番ビルドとローカルFirebaseエミュレーターを組み合わせて操作を確認した。
  • 独立レビューで、1024pxの麻雀卓は左右の座席と中央操作が各5px重なることを発見した。ページの横スクロールだけでは検出できず、部品同士の矩形と操作確認も必要だった。麻雀だけ2列化を1280pxまで遅らせた。
  • クラス文字列の存在だけでは列配置は保証できない。ブラウザで幅、gridTemplateColumns、scrollWidthとclientWidth、操作・履歴の位置を計測した。
  • 既存のre2js監査highがCIを妨げたため、許容範囲内の2.9.1へロックを更新した。監査例外やCI設定は変更せず0件になった。

検証の根拠

ローカルで320/375/768/1024/1440pxを確認し、通常・ゼロサム・麻雀・ポーカー・大会の画面に横スクロールがないことを確認した。通常ゲームは作成・スコア記録・累計・履歴を確認。日本語・英語ホーム、ニュース、案内、ログイン、会員履歴も確認した。会員はエミュレーターでログインし、2件の履歴がスマホ1列・タブレット/PC2列になることを確認。lint、140ファイル1211テスト、型検査を含む284ページの本番ビルド、依存監査が成功した。実メール配送や外部Google認可は今回の表示変更の検証対象ではない。

本番確認の範囲

pointpia.comの日本語/英語ホーム、ニュース一覧・本文、ゲーム紹介、英語ガイド、ログイン、未ログインのマイページを375/768/1440pxで確認した。通常ゲームを新規作成し、10/-3点の記録・累計・履歴を確認。ゼロサム・麻雀・ポーカー・大会もテスト専用ゲームで表示を確認し、大会の広幅表示を維持した。麻雀は1024/1280/1440pxで座席と中央操作の矩形交差なし、局入力と点数ヘルパーの開閉を確認した。

本番の会員ログイン後の履歴・成績は未確認。認証済み会員での表示・2列履歴はローカルエミュレーターで確認済み。本番の新規登録・メール送信・外部Google認可は実施せず、既存ユーザーのゲームは変更していない。アプリ内お知らせは本体の本番確認後の別PRで公開する。

トップページのフォームと説明を1列へ戻す

uxリリース済み

トップページのフォームと説明を1列へ戻す

リリース状況

リリース済み。PR #77をmasterへ実マージし、公開ビルドREADY・実トラフィック100%・ソースコミット80abbc0a4e22de64d37cad99051414f137d6240fの一致を確認した。親が本番の日英トップページを3種類の画面幅で確認し、上下1列と訂正したお知らせを確認した。

何を変えたか

前回のPC向けトップページの2列配置を、ユーザーの追加希望に合わせて全画面幅で1列へ戻した。作成フォーム、ニュース、使い方と説明を上下に並べる。端末別の横幅、フォーム内のカード、ゲーム画面の横並びは維持する。アプリ内の既存お知らせにあるトップページ横並びという説明も10言語で訂正する。

なぜ変えたか

画面幅の活用とページ全体を複数列にすることは別々の判断になる。前回の表示調整を公開した後、トップページは1列にしたいという追加希望を受けた。

ユーザーにとって良くなったこと

スマホでもPCでも、フォームからニュース・説明へ同じ順番で読み進められる。横幅は端末に合わせて広がり、ページ全体の配置は上下にそろう。

技術的な内容・学んだこと

外側のlg:grid/lg:grid-cols-2だけを外し、通常のブロック配置とspace-y-8へ変更する。使い方カードのmt-8/lg:mt-0を同時に除去し、余白を二重にしない。CSSの画面幅ブレークポイントを使う場合も、カード内部の複数列とページ全体の複数列を分けて判断する必要がある。

以前の更新履歴は当時の公開内容として保持し、この修正を追記して方針の変化を残す。お知らせは既存ID・公開日・既読管理を保ち、説明文だけを実際の配置へ合わせる。

検証

ローカルで375/768/1440pxすべてにおいて説明がフォームの下へ配置され、フォーム/説明の幅343/736/1152pxで一致し、横スクロールなしを実測した。トップページとお知らせの既存27テスト・lint警告0、独立レビューの必須指摘なしを確認した。全件CIのテスト・本番ビルド・依存監査・Firestoreルール検証も成功した。本番でも日本語/英語の375/768/1440pxすべてで上下1列・間隔32px・横スクロールなしを確認した。フォームと説明の幅は343/736/1152pxで一致し、フッターのお知らせの訂正文面と開閉も確認した。

PointPiaの会員画面とGoogleリダイレクト認証をリリース

releaseリリース済み

PointPiaの会員画面とGoogleリダイレクト認証をリリース

リリース状況

2026年10月28日にリリース済み。未リリース時の記録は 2026-10-28-member-ui-google-redirect.md に残し、本稿で本番反映と確認結果を追記する。

変更と利用者への効果

ログイン・会員登録、会員ボタン、マイページ、ゲーム内の会員操作を、PointPiaのピンク・白いカード・灰色背景に統一した。成績の数字や履歴の状態を読み取りやすくし、スマートフォンの操作にも合わせた。登録なしでも従来どおり利用できる。

Googleログインは別ウィンドウから同じタブのリダイレクトへ変更した。本番の pointpia.com と www.pointpia.com の両方で、既存Google会員の認証からマイページへの復帰とログアウトを確認した。

技術的な知見

Firebaseの signInWithRedirect と getRedirectResult に加え、本番の同一ホストを authDomain に設定し、Next.jsのrewriteで認証ヘルパーをFirebaseへ転送する。認証ヘルパーは言語middlewareから除外した。Google Cloudには両ホストの /__/auth/handler を承認済みリダイレクトURIとして登録する。

認証監視によるユーザー復元とリダイレクト結果の処理は別々に完了するため、両方を待ってから画面を移動する。React StrictModeでも結果を二重取得しないようAuthインスタンスごとのPromiseを共有した。

HTTP localhostとFirebase標準ドメインの往復ではユーザーが復元されなかった。ローカルでは実Googleへの遷移と、同一ホストのAuthエミュレーターによる往復を分けて確認した。エミュレーター成功を実Google成功として扱わず、本番の独自ドメインで実Google往復を確認して完了とした。

検証・配信の根拠

  • 137ファイル・1,172テスト、lint、型チェック、本番ビルド283ページが成功。
  • ローカルと本番で登録済み5会員のメールログイン・履歴・通算成績・キーなし管理権限復帰を確認。未認証の個人APIは401。
  • 本番ブラウザーで会員導線、メールログイン、履歴、成績、デスクトップと390px表示、ログアウトを確認。
  • 独立レビューの必須指摘を解消し、PR #67をマージ。自動配信workflowの品質確認とdeployがすべて成功。
  • 配信コミット 4c1ade5ee69390b93366d095f619211221de2963 とApp Hostingの稼働コミットが一致。READY・実トラフィック100%を検証した。
  • アプリ内お知らせも同じ配信に含めた。

新規登録・実メール配送・パスワード変更は今回の確認対象外。既存会員の認証情報や個人情報は記事に記録しない。

PointPiaの会員UI統一とGoogleリダイレクト認証を実装

improvement未リリース

PointPiaの会員UI統一とGoogleリダイレクト認証を実装

リリース状況

未リリース。ローカル実装・検証と独立レビューを終え、Draft PR #67を作成した。Google OAuthクライアントの戻り先URI追加・反映待ちのため、マージと本番反映は保留。本番での実Googleログイン完了はまだ確認していない。

何を変えたか

ログイン・会員登録、マイページ、会員ボタン、ゲーム内会員操作を、PointPiaの既存のピンク・白カード・灰色背景へ統一した。スマートフォンでも入力と操作ができる余白を取り、成績の数値を右揃えにした。履歴の確定状態とスコアのラベルを10言語で整理した。

Googleログインはポップアップから同じタブのリダイレクトへ変更し、戻ってきた認証結果とユーザーの復元が完了してから、安全な元のゲームまたはマイページへ移動する構成にした。

なぜ変えたか

会員画面だけボタン・入力欄・カードの見た目が他の画面と揃っていなかったため。認証画面の切替や主要操作を分かりやすくし、Google認証の別ウィンドウを避けるため。

ユーザーにとって何が良くなるか

会員画面を既存のPointPiaと同じ操作感で利用できる。Google認証が同じタブで進み、認証前に開こうとしていたゲームへ戻れる。登録なしの利用と既存の会員履歴は維持する。本番提供は外部設定と公開後の確認が済んでから行う。

技術的な内容

  • 共通の会員レイアウトと操作スタイルを作り、色・枠線・入力欄・フォーカス・無効状態を統一した。
  • FirebaseのsignInWithRedirectとgetRedirectResultを使用する。React StrictModeで認証結果を二重に消費しないよう、Authインスタンスごとの結果取得を共有する。
  • 本番はアプリのドメインをauthDomainにし、Next.jsのrewriteで認証ヘルパーのGET/POSTをFirebaseへ透過転送する。認証ヘルパーを言語middlewareの対象から除外する。
  • メール登録の途中でも認証ユーザーが通知されるため、表示名保存や確認メール処理が終わる前に自動遷移しないようにした。後処理の失敗表示も画面遷移で隠さない。

判断したこと・トレードオフ

リダイレクト関数への置換だけでは、別ドメインのストレージ制限や戻り先の設定を解決できない。同じドメインの認証ヘルパーを使う方式を選び、Google側のcallback URI設定も公開前提とした。

HTTP localhostとFirebase標準ドメインの往復では、アプリ内ブラウザーと通常Chromeで会員を復元できなかった。一方、同じホストのAuthエミュレーターではGoogle認証からマイページへの復帰とログアウトを確認した。これは実Googleでの認証完了の代わりにはならないため、本番の独自ドメインで別途確認する。

ハマったこと・学んだこと

  • 認証監視でユーザーが得られただけでは、リダイレクト結果の処理が終わったとは限らない。
  • 言語変更で翻訳関数が変わっても、認証監視まで作り直して会員セッションを切らないようにする必要がある。
  • Google開始後に既存のメールログインと同じ画面遷移処理を実行すると、外部認証が始まる前にアプリ内を移動する危険がある。
  • ローカルのGoogleへの遷移、エミュレーターの往復、実Googleの本番完了を分けて検証結果に残すことが重要。

PointPiaの紐付けモーダルと30日分のお知らせ一覧を公開

releaseリリース済み

PointPiaの紐付けモーダルと30日分のお知らせ一覧を公開

リリース状況

2026年10月28日にリリース済み。PR #69の自動配信と、本番の稼働コミット・画面表示を確認した。

何を変えたか

ゲームの上部に常時表示していた会員フォームを、「紐付ける」ボタンから開くモーダルへ移した。通常時の会員操作はマイページと合わせて1行にし、既存のゲーム本体の配置を保つ。参加者の選択・紐付け申請・自分の追加・管理者の承認はモーダル内で行う。閉じていても会員の権限取得やポーカーの席復元は続ける。

お知らせは最新1件から、直近30日分を新しい順に表示する一覧へ変更した。同日に複数件の更新があっても過去分を残し、フッターの「お知らせ」から読み直せる。日付は日本時間の今日と過去29日を対象とし、未来日を除外する。操作文言と内容は10言語に対応した。

なぜ変えたか・利用者への効果

会員フォームによってゲーム画面が大きく押し下げられていた。必要な時だけフォームを開くことで、スコア入力や履歴の配置を保てる。

また、同日に何度もデプロイすると最新以外のお知らせを読めなくなっていた。過去1か月分の一覧と常設の再表示ボタンにより、利用者が見逃した更新を確認できる。

技術的な内容

  • 紐付けにはネイティブdialogを使い、背景操作の抑止、Esc、閉じる操作、スクロールとフォーカスの復元に対応した。
  • お知らせは公開日で絞り込み、同日分も別のIDとして保持する。既読状態も最新ID一つからID配列へ変更した。
  • 新しい未読IDがあれば一覧を表示し、期間外になった既読IDが減るだけでは再通知しない。
  • 既読は閉じる時に保存する。旧形式の保存キーは移行互換を保ち、読んでいる途中の再読み込みでは未読状態を維持する。
  • 長文一覧だけをスクロールさせ、閉じる操作は画面内に固定した。

ハマったこと・学んだこと

旧形式の保存キーを表示時に更新すると、新形式の既読配列がまだない利用者は、閉じずに再読み込みしただけで全件既読と判定される。新旧両方の保存タイミングを閉じる時にそろえ、未閉鎖の再マウントと閉鎖後の再マウントを回帰テストにした。

単純な「最新IDが変わったか」だけでは、同日中の複数リリースや履歴の再表示を扱えない。お知らせの保持期間と、利用者が読んだIDを別々に判定することが重要だった。

検証の根拠

  • 全139ファイル・1,187テスト、lint、型検査込み本番ビルド283ページが成功。
  • ローカルエミュレーターで参加者からの申請、管理者による承認、紐付けの保存を確認。
  • PCと390px幅、日本語と英語で一覧・最古の項目までのスクロール・再表示・Esc・フォーカス復帰を確認。
  • 閉じずに再読み込みした場合の通知継続と、閉じた後の自動表示抑止を実ブラウザーで確認。
  • 独立レビューの指摘を修正し、最終コードのCI成功後に公開。アプリ内のお知らせにも今回の変更を記載した。

配信コミットは 681ab65115cf98d49e66c52305d004c4eb082c80。App HostingのREADY・実トラフィック100%・稼働コミット一致を確認した。本番でも月内全件・既読・フッター再表示、指定テスト会員のゲーム表示とモーダル開閉を確認し、ゲームデータは変更していない。

PointPiaでアカウント機能のメール確認を必須化

securityリリース済み

PointPiaでアカウント機能のメール確認を必須化

リリース状況

リリース済み。公開ビルドのREADY・実トラフィック100%・ソースコミットの一致を配信ゲートで確認した。稼働ソースは 41a7991b9ffc4d551c9508d4f4b2cb441a9fe89c。本体はPR #71、入力値の破棄はPR #72で反映した。画面操作によるローカル・本番の動作確認はユーザー担当であり、開発側では実施していない。

何を変えたか

メールでアカウントを作成した直後は確認待ち画面に留まり、確認が終わるまで履歴・通算成績・参加者の紐付けを使えないようにした。既存の未確認アカウントも同じ制限を適用する。保存済みデータは削除せず、登録なしのゲーム利用は維持する。再送、確認状態の更新、ログアウト、登録なしで続ける導線を用意した。

なぜ変えたか

確認メールの送信と案内だけでは、メールアドレスの所有確認前にもアカウント機能を利用できていた。利用開始条件としてメール確認を明示的に実装する必要があった。

ユーザーにとって何が良くなったか

確認前と確認後の利用範囲が明確になり、確認リンクを開いた後は同じ画面から処理を再開できる。元のゲームからログインした場合は確認後も安全な復帰先を引き継ぐ。

技術的な内容

Firebaseの可変Userから独立したisVerifiedスナップショットで画面を制御し、未確認ユーザーにはアカウント用通信セッションを発行しない。サーバーでは失効チェック付きIDトークン検証に加え、email_verifiedがtrueであることを必須にする。画面を迂回した直接リクエストも拒否する。Firestoreのクライアント直接アクセス禁止は維持する。

確認リンクのクリックだけでは、開いている画面のユーザー情報や既存IDトークンは新しくならない場合がある。reloadに続けてgetIdToken(true)を実行し、同じユーザーのままであることを確認してからアカウントセッションを有効にする。

FirebaseのUserオブジェクトは同じ参照のまま更新されるため、Reactには、確認トークン更新が成功した時点で確定するisVerifiedを含んだ状態を渡す。reloadだけでtrueになった可変Userの属性をUIの利用許可として直接参照しない。Googleログインも確認済みフラグで同じ判定を使う。Firebaseコンソールの追加設定は不要。

ハマったこと・学んだこと

登録処理の確認メール送信に失敗したときのエラーを保持したままだと、後から確認を済ませても遷移条件を満たせない。未確認時の制限と、確認済みになった後の復帰条件を分け、送信失敗からの復帰を回帰テストにした。

認証更新中のログアウトや別アカウントへの切り替えでは、処理開始時の古いユーザーを再びセッションへ入れないことも必要になる。更新後に現在のユーザーを再照合する。

検証の根拠

未確認トークンの403、履歴・紐付けAPIの拒否、確認待ち画面、認証トークンの強制更新、更新中のログアウト、メール送信失敗からの復帰を自動テストにした。全140ファイル・1,211テスト、lint、型検査込み本番ビルド284ページ、独立レビューとPRのCI成功を経て反映した。実登録・実メール送信・手動の画面確認は実施していない。

確認待ちUIに入力値を残さない

確認待ちのカードへ表示を切り替えても、親のログインフォームはアンマウントされない。パスワードをReact stateに残すと、ログアウト後に入力済みのフォームが戻る。この追加指摘は、ユーザーIDが設定された時点で入力値を破棄して修正した。ログインから確認待ち・ログアウト・フォーム再表示までの回帰テストで、入力欄が空になることを検証した。

PointPiaに任意の会員登録・ゲーム履歴・通算成績を追加

feature未リリース

PointPiaに任意の会員登録・ゲーム履歴・通算成績を追加

リリース状況

未リリース。ローカル実装・自動検証・demoエミュレーターでの通し確認まで実施した。Firebaseのログイン方法や許可ドメインの設定は利用者側で行うため、設定完了までアプリのマージと本番反映を保留する。実Google認証、実メール配送、本番動作は未確認。

何を変えたか

  • 未登録のゲーム作成・記録・共有を維持し、Googleとメール/パスワードによる任意の会員登録を追加。
  • 会員に作成・参加ゲームの履歴とモード別の通算成績を提供。
  • 自分を新規参加者として追加した場合は自動で紐づけ、既存の参加者枠はゲーム管理者の承認で紐づけ。
  • 管理権限をアカウントへ保存し、別端末でも会員として復帰。
  • メール確認・確認メール再送・パスワード再設定に対応。
  • ゲーム種別に応じた結果確定・確定取消を用意し、進行中の結果を確定成績から除外。

なぜ変えたか

招待リンクと端末内の管理キーによる気軽な利用を残しながら、自分の過去ゲームや戦績を端末をまたいで見られるようにするため。登録必須にはせず、保存が必要な人だけ登録できる形を選んだ。

技術的な内容

Firebase AuthenticationのIDトークンを、ゲームの招待キーとは別ヘッダーで送信する。サーバーではAdmin SDKの失効チェック付き検証から会員IDを得て、ゲームごとの会員権限を解決する。名前やリクエスト本文の会員IDを本人確認に使わない。

ゲーム内の表示名とは別に安定した参加者IDを保持する。既存枠の申請・承認はFirestoreトランザクション内で処理し、一つの枠を複数会員に紐づけない。一般の参加者追加で、主催者が他人の名前を自分として取得することも避ける。

一覧用の会員別索引と、ゲーム内の権限・本人対応の正本を分ける。通算成績は最初の一覧ページだけではなく、本人の全保存ゲームから計算する。結果は正本から計算し、修正や確定取消による二重計上を避ける。

通常とゼロサムはラウンドの合計、麻雀は既存のウマ・オカ精算、ポーカーキャッシュは購入履歴と残高、大会は既存の順位計算を利用する。異なる単位のスコアを合算しない。

判断したこと・トレードオフ

  • 会員登録だけでは戦績保存は実現しない。本人とゲーム内参加者の対応、確定状態、権限復帰を一緒に設計した。
  • 招待リンクを持つことと、そのゲーム内の特定参加者本人であることを分けた。
  • 会員由来の新しい管理キーや席のキーは会員セッション内で保持し、ログアウトや会員切替で破棄する。既存のゲスト用招待リンクは別の権限として従来どおり扱う。
  • 直接のFirestoreアクセスを会員向けに開放せず、既存のサーバーAPI経由を維持した。
  • 都度集計は修正との整合を取りやすい一方、保存ゲームが増えると読み取り量が増える。将来の集計キャッシュ導入では更新と無効化の設計が必要になる。

ハマったこと・学んだこと

  • ポーカー大会はゲームの権限だけでなく、端末内の大会管理キーも確認していた。別端末への会員復帰には、この二つの認証経路をつなぐ必要があった。
  • 会員IDを表示名で代用すると、改名や同名の人で成績が混ざる。表示名を変更できる機能では、本人対応の安定IDが必要になる。
  • ログアウト時の画面消去だけでは、処理中の古いトークン取得やリクエスト応答が残る。セッションの世代と現在の会員を確認して、古い処理を表示へ戻さないようにした。
  • ローカルのNode.jsと依存関係がプロジェクト指定と違うと、既存テストまで失敗する。指定Node.jsとlockfileを揃え、環境要因と機能の失敗を切り分けた。
  • 本番プロバイダーが未設定でも、AuthとFirestoreのdemoエミュレーターなら本番設定を変更せずに登録・紐づけ・成績まで通して確認できる。ただし実Google認証と実メールの確認の代わりにはならない。

PointPiaの会員履歴・通算成績を本番で検証して公開

releaseリリース済み

PointPiaの会員履歴・通算成績を本番で検証して公開

リリース状況

リリース済み。PR #64をmasterへマージし、Firebase App Hostingの実トラフィックが対象コミットへ100%切り替わったことを確認したうえで、既存の検証用5会員による本番動作を確認した。先行する未リリース記録は履歴として保持する。

何を変えたか

登録なしで遊べる従来の利用に、会員のゲーム履歴・ゲーム種別別通算成績・別端末からの管理復帰を追加した。既存参加者との紐づけは管理者の承認、新規の自己参加は自動紐づけとした。

なぜ変えたか

気軽に始める利用と、継続して成績を残す利用を両立するため。

ユーザーにとって何が良くなったか

自分が参加したゲームと成績をマイページから振り返り、会員として作成したゲームの管理を別の端末で再開できる。登録は任意のまま。

技術的な内容

Firebase Authenticationの本人確認とゲームの招待キーを分離する。ゲーム内の会員権限と安定した参加者IDを正本とし、会員別の履歴索引を持つ。結果は各ゲームの正本から集計し、確定取消や結果修正へ追従する。

Before / After

Before

履歴や管理権限の復帰が端末内の情報に依存していた。

After

会員はログインによって履歴と権限へ復帰できる。通常・ゼロサム・麻雀・ポーカーキャッシュ・大会の成績は種別ごとに計算する。

判断したこと・トレードオフ

  • メールアドレスの一致や同名だけで既存参加者を本人として取得しない。管理者承認を挟む。
  • 登録処理の確認と、登録済み会員による利用の確認を分けた。今回の実Firebase検証では新規登録・メール送信・パスワード変更を行っていない。
  • デプロイ開始・ビルド成功だけを公開完了にせず、実トラフィックとソースのコミットを照合してから機能確認した。

ハマったこと・学んだこと

  • 認証状態の初期復元中にゲームを作ると、保存済み会員がゲストとして扱われうる。復元完了まで作成を待機する。
  • 会員切替の後に古い通信が戻る場合がある。応答の世代と会員IDを照合し、他の会員へ履歴や権限を持ち越さない。
  • ポーカーでは現在の席と過去に着席した席の集合が異なる。席復帰と全席の損益集計を分ける必要がある。
  • 同じFirebaseを使うローカルと本番の確認では、検証ゲームの名前を分けると、双方のデータを区別しながら同じアカウントの復帰を確認できる。

PointPiaの10言語対応を本番へ反映

releaseリリース済み

PointPiaの10言語対応を本番へ反映

リリース状況

リリース済み。2026年10月26日21:04 JSTに本番URLの確認を完了した。最終配信コミットは 920552713d6054ccd6cd7af1528e02b790552098、App Hostingのビルドは build-2026-10-26-006。公開ワークフローが、READYビルドにトラフィックが100%集約され、コミットが一致することを検証した。PR #62と追加修正のPR #63はマージ済み。

実装時の未リリース記録 2026-10-26-complete-localization.md は履歴として保持する。本番へは10言語のアプリ内お知らせも同じ成果物で公開し、公開JavaScript内に対象のお知らせIDが含まれることを確認した。

何を変えたか

  • 追加7言語に残っていた英語のガイド・ヘルプ・規約・問い合わせ・ゲーム操作文を翻訳した。
  • 大会・通常ポーカー、認証・エラー、支援決済、共有PNG内の固定文言を10言語で表示する。
  • URL・保存Cookie・ブラウザ言語に応じて言語を判定し、変更時はゲームのパス・クエリ・招待フラグメントを維持する。
  • 日時は端末のタイムゾーン、数値は選択言語の書式に合わせる。共有PNGの点数は整数部分の地域別整形と小数文字列を分けて精度を保持する。
  • ニュースの記事・一覧・RSSは利用者の指定どおり日本語を維持する。

なぜ変えたか

辞書のキーが揃っていても、本文の英語コピーや画面への日本語直書きは残る。海外利用で実際に触れる画面、失敗時の案内、決済ページや共有画像まで対象を広げる必要があった。

ユーザーにとって何が良くなったか

選択した言語を保存でき、ガイドからゲーム操作、失敗時の案内、結果画像まで同じ言語で利用できる。海外の端末でも時刻や桁区切りを読み取りやすくなった。支援決済はJPY建てを維持し、商品名・Checkout・入力エラーを選択言語に合わせた。

本番で見つけた問題と修正

最初の公開確認で、他言語ニュースURLのLocationに /ja/news,/ja/news が含まれる応答を検出した。対処として日本語専用ニュースをnext-intlの言語書き換えより前にMiddlewareで処理し、他言語は単一308、日本語は書き換えなしとした。記事・RSS・クエリを維持し、存在しない他言語版のhreflangや言語Cookie変更も防いだ。回帰テスト4件を追加し、再公開後は9言語のニュースURL、RSS、記事の転送がすべて成功した。

検証した内容

  • ローカル: TypeScript型検査、警告ゼロのESLint、126ファイル・1,099テスト、本番ビルド成功。
  • PRと公開ワークフロー: Quality GateとFirestoreルール検証に成功。App Hostingロールアウトと配信コミット照合にも成功。
  • 本番HTTP: 10言語×6ページの見える本文、言語優先順位、ニュース・記事・RSS転送、ゲーム画面のprivate/no-store、英語・ドイツ語の決済API入力エラーなど79項目に合格。
  • 公開ワークフローで全39ニュース記事の本文・リンクを照合した。
  • 本番アセットに10言語のお知らせが含まれることを確認した。

ブラウザでの操作検証、実決済、利用中ゲームのデータ更新は実施していない。決済APIには不正金額のみを送り、支払いを作成していない。

ハマったこと・学んだこと

リポジトリには旧HostingとApp Hostingの両経路があり、自動公開フラグは未設定でも本番DNSはApp Hostingへ切り替わっていた。旧Hostingワークフローも手動停止されていたため、既存App Hostingワークフローを手動実行した。DNSと自動公開切り替え設定は変更していない。設定ファイルだけで配信先を推測せず、実運用状態と配信中のSHAを照合する必要がある。

また、単体テストやビルドの成功だけでは、配信アダプターを経由したHTTP転送の挙動まで保証できない。公開後の確認で検出した問題を回帰テストへ落とし、修正後の公開URLを再検証してから完了とした。

PointPiaの10言語対応を本文・操作画面・地域別書式まで揃える

improvement未リリース

PointPiaの10言語対応を本文・操作画面・地域別書式まで揃える

リリース状況

未リリース。専用ブランチ codex/complete-localization で実装し、型検査、ESLint、Vitest、Next.jsビルドを検証した。本番反映は行っていない。リリース済み情報のみを載せるアプリ内お知らせには、この変更をまだ追加していない。

何を変えたか

日本語・英語・中国語・スペイン語・フランス語・ドイツ語・韓国語・ブラジルポルトガル語・イタリア語・ロシア語について、既存の辞書と画面を修正した。追加7言語で英語のまま残っていたガイド、ヘルプ、プライバシー、規約、問い合わせ、ゲーム操作の本文を翻訳した。

大会の構成編集・会場表示・管理操作、通常ポーカーのタブ・状態・注意文・アクセシビリティ名、認証、404・例外ページ、支援完了画面、共有PNG内の見出しを翻訳した。ニュースの記事・一覧・RSSは、運用の難しさから日本語のままにするという利用者の指定を維持した。既存の10言語の更新お知らせも維持した。

なぜ変えたか

翻訳ファイルのキーが揃っていても、見出しだけ翻訳して本文を英語からコピーした箇所や、JSXへ直接書いた日本語は残る。入口URLが常に日本語へ転送され、履歴は全言語で日本時間になっていたため、海外利用の体験を画面から見直した。

ユーザーにとって何が良くなったか

言語なしのURLでは保存した言語またはブラウザ言語を使い、明示された言語URLを優先する。選択言語をCookieへ保存し、変更時にゲームのパス・クエリ・招待フラグメントを維持する。

履歴時刻は端末のタイムゾーンで表示し、日付・数値を選択言語の規則へ合わせる。支援はJPYのまま、商品名・Checkout画面・失敗メッセージを選択言語へ合わせる。未知のサーバーメッセージは直接出さず、権限不足・状態競合・入力不備・通信障害などを翻訳した案内に変換する。

技術的な内容

  • next-intlの辞書へ操作・例外用のInterfaceセクションを追加した。
  • Next.jsの言語固定redirectを必要な旧URLと日本語ニュースに限定し、next-intl Middlewareで言語を判定する。
  • Intl Provider外でも表示されるエラー画面では、安全に言語を解決して同期辞書から表示する。
  • 大会の既定のLevel N / Break Nは表示時に翻訳し、利用者が入力した独自名を保持する。
  • 共有画像へ翻訳ラベルとlocaleを渡す。点数は整数部分をBigIntからIntlで整形し、小数部分を文字列で保持することで、不必要な丸めを防ぐ。科学表記は元の文字列を保つ。

ハマったこと・学んだこと

翻訳キー・プレースホルダーの一致だけでは本文の翻訳漏れを検出できない。英語と同一の長文の検査、全メッセージのICU構文解析、実辞書を使う非日本語の操作テストを組み合わせた。同綴の用語や固有名詞は明示的に扱う。

数値の区切りを手動置換する場合、スペイン語・イタリア語の4桁の整数では区切りが入らないことがある。区切り文字だけを取得して3桁ごとに挿入する方法では不十分だった。また、点数全体を通常のIntl既定精度で処理すると小数を丸めるため、整数の地域別表示と小数の精度保持を分けた。

SWCの初期エラーは依存パッケージの破損ではなく、制限されたユーザーキャッシュへの書き込みが原因だった。検証時は許可された実パスの一時領域を指定した。ホストのNode.jsのWeb Storageとテスト環境の衝突も、検証コマンドのオプションで分離した。アプリの依存バージョンは変更していない。

動作確認

型検査、警告を許容しないESLint、全Vitest、Next.jsの本番ビルドを実施。日本語以外の画面、言語判定と保存、招待URLの維持、日時・数値書式、決済リクエスト、共有PNGへ渡す文言をテストした。ブラウザでの実機確認と本番デプロイは行っていない。テスト成功はネイティブ話者による全訳文の校閲や、本番提供の確認を意味しない。

対戦結果をSNS向けのリザルトカードとして保存・共有

featureリリース済み

対戦結果をSNS向けのリザルトカードとして保存・共有

リリース状況

2026-10-25 本番リリース済み。 PR #55の初回反映後、PR #56で小数の丸め・長い数値の省略を修正し、新機能と最近の未告知変更のお知らせを全10言語で公開した。最終マージ ecd19a95870aa63f62744adf8e5a3c9394405742 の自動デプロイrun 40153439594 は全ジョブ成功。両担当のLint・118ファイル962テスト、実装担当の本番ビルド、独立したローカル実操作に加え、独立した本番の実API・縦横PNG・お知らせ表示・既読動作の確認まで完了した。必須指摘R1/R2は本番でも解消し、残る指摘はない。

本番では専用の通常・麻雀ゲームを使い、API応答を置き換えずに画像生成・保存・名前非表示・X投稿文を確認した。通常ゲームでは1.232/1.231/0.004/-0.004と、1e+21/1,234,567.8901/1e-30/-12,345.6789を実データに設定し、縦長・横長双方で全文が描画されることを確認。お知らせは全10言語・320×320の画面でスクロール・末尾の閉じるボタン・旧既読IDからの更新・再読み込み後の既読抑止を確認した。実スマートフォンでの共有先選択・実SNS投稿は未実施。

何を変えたか

ゲーム画面から、全員の結果または選択したプレイヤーの成績をPNG画像にする機能を追加した。縦長1080×1920と横長1200×675を用意し、ゲーム名、日付、順位、スコア、一言コメントを載せる。名前は匿名の番号へ切り替えられる。

見た目はアイボリーを基調に、淡いラベンダーとピーチのグラデーション、余白、大きな順位・数字を組み合わせる。大人数の結果は複数ページへ分ける。

なぜ変えたか

これまではゲームのURLとQRコードによる参加・閲覧共有が中心だった。XやInstagramのストーリーに、対戦の記録や自分の好成績を見栄えよく投稿したいという要望に対応するため。

ユーザーにとって何が良くなったか

手作業でスクリーンショットを切り抜かず、SNSに適した比率の画像を作れる。全員の記録と個人成績を用途で選び、名前の表示やコメントを調整できる。端末がファイル共有に対応していれば共有メニューから送れ、保存した画像をSNSアプリで選ぶ使い方もできる。

技術的な内容

TypeScriptの純粋なカードモデルで、順位、匿名化、プレイヤー選択、ページ分割を処理する。Canvas 2Dで描画し、PNG Blobから作った同じファイルをプレビュー・ダウンロード・OS共有に用いる。外部の画像生成サービスや画像アップロード用のサーバーは追加しない。

共有用モデルへ明示した表示データだけを渡す。ゲームのURL・QR・権限トークンを画像や自動生成する投稿文へ含めない。利用者自身がゲーム名やコメントに入力する文章は別途プレビューで確認する必要がある。

Before / After

Before

共有の中心は、ゲームへアクセスするためのリンクとQRコード。SNS用の結果画像は用意されていなかった。

After

ゲーム画面から縦長・横長のPNG画像を作り、全員版・個人版・名前表示を選択できる。Xの投稿画面へ渡す文章と、保存した画像の添付は別の操作として案内する。Instagramの特定の投稿画面への自動遷移や投稿完了は保証しない。

判断したこと・トレードオフ

  • HTMLの見た目を別の画像へ変換する方式ではなく、保存対象のPNGそのものをプレビューする。プレビューと保存結果の不一致を避けやすい。
  • 端末の共有機能とPNG保存を併設する。SNSのアカウント接続や投稿APIの認証を必須にしない。
  • 同点は同順位として扱う。ゲーム固有の順位がある場合は、その計算結果を利用する。
  • ポーカーは精算済みの状態、または大会終了後の確定順位を使う。不正確な途中経過を確定成績として出さない。

ハマったこと・学んだこと

Web Share APIには一時的なユーザー操作の権限が必要で、クリック後にPNG生成の非同期処理を挟むと共有が拒否されることがある。プレビュー生成時にFileを準備し、共有ボタンから待機処理なしで共有を呼び出す構成にした。共有のキャンセルと、画像生成・共有の失敗は区別する。

画像生成中に選択項目や成績データが変わると、古い画像生成の完了が新しい状態へ混ざる可能性がある。保存対象とモデルの対応を確認し、破棄済みの生成結果を無視する必要がある。

日本語表示ではフォント準備を待ち、長い名前・コメントは描画幅に合わせる。名前非表示と画像のページ分割は、画面だけでなく保存対象のカードモデルへ適用する。

最初の画像確認では整数と麻雀の小数を中心に検証していたが、追加確認で小数3桁以上の精度低下を発見した。描画側の maximumFractionDigits: 2 により、通常画面では異なる1.231と1.232が画像では両方1.23になり、0.004は0になる。順位計算が正しくても、表示が丸められるだけで結果の意味が変わる。元のNumberの文字列表現を保ち、整数部分だけ桁区切りする共通フォーマッターへ変更した。主役の大きなスコアと一覧行の両方を、ローカルと本番の実PNGで再検証した。

フォーマッターの修正後にも、横長の一覧では描画共通処理が18pxまで縮小した後に末尾を省略し、1,234,567.8901が1,234,567.8…となる問題を発見した。数値文字列が正しくても、保存する画像が正しいとは限らない。名前・コメントに使う省略処理とスコアの描画を分け、数値は9pxまで縮小し、Canvasの fillText の幅指定でも領域内へ収めて全文を描くようにした。縦長・横長それぞれの実際の描画文字列、幅指定、PNGサイズを連携テストで検証する。

画像付き共有メニューと任意の閲覧用QR

featureリリース済み

画像付き共有メニューと任意の閲覧用QR

リリース状況

2026-10-25 本番リリース済み。 ユーザーがSNSログインを不要とする画像付き共有メニュー方式を選択し、画像へ閲覧用QRを任意で含める追加要件と合わせて実装した。コミット 491cb845e815f93a32c9065f98b3d8fa6fd1b493 はローカル確認と独立した最終再レビュー、PR #58の必須CIを通過。マージ d9cdf475211dc46f7dcc4350c93f4fb357d373fd に対する自動デプロイ 40180573167 は全3ジョブ成功し、独立した本番画面の確認も合格した。

何を変えたか

X向けは表示中の結果画像と投稿文、Instagramストーリー向けは表示中の画像を端末の共有メニューへ渡す。利用者が共有先を選び、SNS側で内容を確認して投稿する。特定アプリやストーリー投稿画面への直接遷移、投稿完了を保証する機能としては扱わない。

画像に閲覧用QRを含めるチェックボックスを用意する。初期状態はオフで、オンのときだけ閲覧権限のリンクをQRとして画像に含める。編集用・管理用のリンクを代用せず、閲覧用リンクを使えない場合は理由を表示する。新たな閲覧キーの発行や既存キーの再発行は、この操作から行わない。

なぜ変えたか

Xの投稿文を開くだけでは、画像を別途選ぶ必要があった。画像を端末からSNSへ渡す操作を分かりやすくするとともに、画像を見た人にゲームを閲覧してもらいたい場合だけQRを付ける要望へ対応するため。

ユーザーにとって何が良くなったか

対応端末ではSNSへ画像を渡しやすくなる。端末が画像共有に対応しない場合にはPNG保存後にSNSで選ぶ方法を案内する。Instagram向けには9:16の画像が選べることを案内し、プレビューとは別の画像を勝手に送らない。

QRを載せる場合、画像からゲームを閲覧できることと、画像内の名前非表示がリンク先ページまで匿名化するものではないことを確認できる。

技術的な判断

Web Share APIのShareDataに渡すファイルは、画像生成時にあらかじめ準備する。クリック後に画像生成を待つ処理を入れず、ユーザー操作に必要な一時的な権限を維持して共有する。

ファイルだけを渡す場合と、ファイルに投稿文を添える場合では、端末の対応状況が異なることがある。それぞれの共有データについてcanShareを確認する。共有のキャンセル、同期的な例外、非同期の失敗を区別し、共有メニューへ渡せたことを投稿完了と混同しない。

QRは既存のqrcodeライブラリでモジュールを生成し、Canvasへ白黒のセルとして直接描く。外部のQR画像サービスへ閲覧用リンクを送らない。静穏領域を確保し、縦長・横長のレイアウトではコメントや順位一覧とQRの領域を分ける。

閲覧用リンクの変化はuseSyncExternalStoreで購読する。同じタブの更新通知、別タブのstorageイベント、画面へ戻るときのfocusを用いて再評価し、通知イベントにはトークン値を載せない。QRをオフにしたままならリンクの変化だけで画像を再生成しない。

設計上の注意

閲覧用QRは、以前の「権限リンクを画像に含めない」という既定動作への明示的な選択肢である。オフ時は画像モデルへ閲覧用URLを渡さず、オン時に限って含める。自動生成する投稿文へは、QRのオン・オフにかかわらず権限リンクを追加しない。

権限リンクが更新された場合に画像のQRも更新し、古い画像をそのまま共有しない必要がある。同じ成績の定期更新では生成済み画像を維持しつつ、実際の画像内容やQRリンクの変更では再生成する、前回の安定性修正との両立も必要になる。

実装担当のローカル確認

Lint、118ファイル980テスト、本番ビルドの型検査と258ページ生成が成功した。ChromeでGET応答を合成データへ置き換え、書き込みを遮断し、同じ成績を複数回取得しても画像が安定することを確認した。X向けとInstagram向けが同じ準備済みFileを使い、プレビューのPNGと一致すること、画像共有に非対応の場合に保存手順が表示されることも確認した。

全員・個人それぞれの縦長・横長、計4枚のPNGを保存し、QRデコーダーで閲覧用URLのみが復元されることを確認した。QRをオフにした画像からはQRを検出しなかった。復号確認に使うライブラリは一時的な検証環境へ配置し、アプリの依存には追加していない。実スマートフォンでの共有先選択やSNSへの実投稿を行った検証ではない。

レビューで見つかったこと

QRをオンにした後で閲覧用リンクが失われる場合、古い画像の保存・共有は停止できていたが、画像領域が「作成中」のままになっていた。実際には生成処理を開始しないため、待ち続ければ完了するように見える説明は不正確だった。

独立レビューの任意指摘Q1を採用し、リンクが利用できない理由と、チェックを外せばQRなし画像の作成・保存・共有へ戻れることを全10言語で案内するように修正した。生成が始まらないことと、オフにして復帰できることも回帰テストとブラウザーで確認した。

独立担当は初回のLintと118ファイル980テストに加え、最終Q1修正後の関連4ファイル43テストを実行した。最終ビルドでも共有ファイルのバイト一致・一時的ユーザー操作権限・4種類のPNGからのQR復号・オフ時のQR不在・共有能力別の代替案・例外処理と二重共有防止を確認。同じタブ、別タブのstorageイベント、画面へ戻ったときのfocusイベントによる閲覧リンク更新、リンク消失時の古い画像停止とQRオフでの復帰も確認した。旧形式のゲームと管理用・編集用リンクしか持たない場合には、閲覧QRを有効にせず代用しないことも確認している。

本番確認とお知らせ

独立担当は対象マージの自動デプロイ成功を照合した後、本番配信コードへ合成GETデータを渡し、非GETと対象外APIを遮断して確認した。定期同期時の安定性、プレビューと共有ファイルのバイト一致、X・Instagramの共有データの違い、例外・キャンセル・二重共有の防止、4種類のPNGのQR復号、QRのオフ、権限リンクの更新・消失・オフ復帰はすべて成功した。旧形式や管理用・編集用リンクしか持たない状態での無効化・理由表示も確認した。

この検証は本番に配信されたUIと合成データの確認であり、共有APIの呼び出しもモックで受け取っている。本番の実API、実ユーザーデータ、実スマートフォンのOS共有先選択、SNSへの投稿完了を検証したものではない。新しい本番データの作成・変更は行っていない。

新機能のお知らせ 2026-10-25-sns-image-share-viewer-qr も全10言語で公開した。独立担当が320×320の画面で、スクロールして末尾から閉じられること、既読後の再表示抑止、閉鎖後のページスクロール復帰を確認した。4枚のPNGとリンク消失時の案内、お知らせの目視でも、切れや重なりはなかった。

結果画像のちらつきを調査し、意味のある変更だけを再描画する

fixリリース済み

結果画像のちらつきを調査し、意味のある変更だけを再描画する

リリース状況

2026-10-25 本番リリース済み。 利用者から結果シェア画像が表示・非表示を繰り返すとの報告を受け、Reactコンポーネントの再現テストで原因を確認した。修正コミット b31badc445be4133c5befbfb4b2fe45cdfdbc863 はローカル確認・独立レビュー・PR #57の必須CIを通過し、マージ 07e162c4f4b008523e37d5770e068d82132d1c7c に対する自動デプロイ 40173700223 は全3ジョブ成功。独立した本番画面の確認も合格した。

何を変えたか

ゲーム画面の定期更新で成績が変わらない場合、生成済みの結果画像を表示し続けるようにした。成績・名前・画像設定の実際の表示内容が変わる場合だけ画像を作り直す。画像の順位は1〜9を01〜09とせず、1〜9で表示する。常に途中状態として扱うゲームでは、画像と自動生成する投稿文から「途中経過」を外した。最終結果が確定したゲームの表示は維持する。

なぜ変えたか

ゲーム画面は複数端末の変更を追従するために定期的にデータを更新する。結果画像のダイアログを開いたまま何も操作しなくても、自動更新だけで画像が消えて再表示されていたため。

ユーザーにとって何が良くなったか

表示中の画像が安定し、確認・保存・共有の操作が途中で無効になりにくくなる。順位と日付周辺の表示も簡潔になる。

調査で確認したこと

GameClientは3秒間隔で同期し、再描画の際にmap/flatMapで共有用プレイヤー配列を作る。配列の中身が同じでも参照は新しくなる。ResultShareDialogのuseMemoはその参照を依存にしていたため、共有カードの参照も変わった。

カードが変わった瞬間に古いPNGの保存を防ぐため、生成済みカードと現在カードの参照が一致した場合だけ画像を表示する仕組みになっている。無関係な再描画でも参照が変わると、その安全策が働いて画像を隠し、生成effectが旧Blob URLを破棄して再生成していた。

一時的な再現テストでは、同じ内容のプレイヤー配列を3回渡し直した。3回とも画像が非表示になり、初回を含め4回生成され、3回URLが破棄された。利用者が報告した表示・非表示の繰り返しを、成績変更なしで再現できた。

ハマったこと・学んだこと

古い非同期処理の結果が最新画像を上書きすることを防ぐ仕組みだけでは、表示の安定性は保証できない。定期同期を持つ親コンポーネントから毎回新しい配列を受けると、参照の変化を内容の変化として扱う画像生成は不要な再実行を繰り返す。

操作直後に画像が出ることだけでなく、ダイアログを開いたまま定期更新を複数回通して、画像・保存ファイル・URLが保持されるか検証する必要がある。同時に、実際の成績変更時には古いファイルを保存・共有できず、遅れて完了した古い生成結果を採用しないことも確認する。

修正の内容とローカル確認

描画に使うカードモデルを生成した後、その内容が現在のカードと等しい場合は現在の参照を維持する。実際のカード内容が変わった場合だけ新しい参照にすることで、古い画像を即座に無効化する既存の判定を保ちながら、不要な再生成を防いだ。JSONの比較は既知の構造を持つ小さな表示用モデルに限定し、プレイヤー配列や画面全体の状態をそのまま比較対象にしない。

実装担当はLint、118ファイル967テスト、258ページの本番ビルドを確認した。Chromeで全API応答を合成データに置き換え、非GETリクエストを禁止した上で、10秒間・3回の定期更新中にBlob URL・画像DOMが維持されること、縦横PNGの順位がゼロ埋めされず「途中経過」がないことを確認した。本番データを用いた検証ではない。

独立担当もLintと118ファイル967テストを実行し、独自のChrome確認で同値の複数回同期中の画像DOM・Blob URL・Fileが保持されること、実スコア変更直後の古い保存・共有が無効になること、遅れた古い生成結果が新しい画像を上書きしないことを確認した。

本番確認とお知らせ

独立担当は対象マージの自動デプロイ成功を照合後、本番配信コードに合成GETデータを渡し、非GETと対象外APIを遮断して検証した。定期更新中の画像DOM・URL・Fileの維持、実スコア変更時の古い保存・共有の停止、古い生成結果の破棄、縦横PNGの順位・文言・日付表示を確認。麻雀・大会の最終結果は残り、大会が進行中の状態になったときは古い画像を利用できなくなることも確認した。

本番の実APIや保存済みデータの動作を検証したものではなく、API応答を合成データへ置き換えた本番UI検証である。新しい本番データは作成せず、利用者データへの書き込みも行っていない。実スマートフォンのOS共有先選択・実SNS投稿は未実施。

修正を案内するお知らせ 2026-10-25-result-share-preview-stability も全10言語で公開した。独立担当が320×320の画面でスクロール・閉じる操作・旧既読IDから新IDへの更新・再読み込み後の既読抑止を確認し、ページエラーはなかった。SNS連携方式の変更はこのリリースに含まない。

結果画像の再生成中も表示領域を予約する

fixリリース済み

結果画像の再生成中も表示領域を予約する

リリース状況

2026年10月25日にリリース。ユーザーから、結果画像の再生成中に画像が一瞬消えることで画面が上下に動くという報告があった。修正PR #59をCI成功後にマージし、マージコミット db68e05abbbab0c4e5ffb8ab800c269fc74d94b7 の自動デプロイは全3ジョブ成功を確認した。本番配信コードを用いた独立動作確認も成功した。

何を変えたか

結果画像を読み込む前から、選択中の縦横比と画面の幅・高さに応じてプレビュー領域を確保するよう変更した。同じサイズを選んでいる間は、初期生成・再生成・失敗・閲覧QRのリンク欠落・画像表示後で枠の寸法を変えない。

古い画像の保存や共有を停止する既存の仕組みは維持する。以前の画像を表示し続ける方法ではなく、空いている領域を保ちながら状態を案内する。

なぜ変えるか

成績が同じ定期更新では画像を再生成しない修正は済んでいるが、成績や設定が実際に変わる場合には再生成が必要になる。その際に画像を外すと、画像の実寸で決まっていた親要素の高さが最小値まで縮んでいた。

ユーザーにとって何が良くなるか

画像を更新しても、その下の操作欄やスクロール位置が急に動きにくくなる。読み込み中に画面が上下へ跳ねる感覚を減らせる。

原因の確認

実装担当が修正前のChromeでreadyとloadingの寸法を測定した。390×844の縦長表示では枠が約388.58px、横長では約23.38px縮んだ。1280×900では縦長425px、横長157.25px、320×480の縦長では152pxの差があった。モバイルで下の操作欄も同じ量だけ移動した。320幅の横長では既存の最小高に収まり、元から差はなかった。

さらに320×480の横長で最下部までスクロールして測ると、画像枠が元から同寸法でもスクロール位置が動いた。共有非対応の状態は806から739へ67px、対応状態は847から739へ108px移動した。再生成中に共有ボタンや代替手順の案内が消え、ダイアログ全体の高さが縮んで、スクロール位置が上限へ押し戻されていた。

技術的な方針

画像の固有寸法を取得してから親の高さを決めるのではなく、既知の出力サイズから縦横比を決める。利用できる幅と既存の最大高さを満たす枠を常設し、画像と状態メッセージをその中へ配置する。

小さい横長枠に長いエラー説明を出す場合も、枠を広げずに文章を読めるようにする。画像の読み込み中だけでなく、生成失敗やQRリンク欠落など、画像が表示されない状態も確認する。

一度確定した共有可否による表示構成は、再生成中も維持する。ただし画像ファイルを渡すデータは無効にし、共有ボタンは押せない状態にする。見た目の領域を保つことと、古い画像を操作できることを分ける。

ハマったこと・学んだこと

同じ内容の再生成を防いでも、内容が変わるときのレイアウト変動は別の問題として残る。画像が出た瞬間だけを見る検証では見つけにくいため、意図的に生成を遅らせ、表示前後の枠・操作欄・スクロール位置を実ブラウザーで測る必要がある。

修正後のローカル検証

実装担当のChrome確認では、320×480・390×844・1280×900それぞれの縦長・横長の6ケースで、同じサイズの画像を再生成した前後のプレビュー寸法と操作欄の位置の差が0pxになった。モバイル2画面幅・2形式・画像共有の対応/非対応を組み合わせた8ケースでも、最下部のスクロール位置の差は0pxだった。

生成失敗と再試行・QRリンク欠落・画像デコード前も6ケースで枠寸法を維持した。320×320の低い画面に長い状態説明を表示した場合も、先頭が枠内にあり、必要時は枠内をスクロールして末尾まで到達できた。ブラウザーの未処理エラーは0件だった。

最終コミットでlintと118ファイル・982テスト、および258ページのビルドは成功。別担当もコードと関連28テストを確認し、ブラウザーで寸法・最下部のスクロール・共有ボタン無効化を独立検証した。未解決指摘は0件。本番の実ユーザーデータは使用していない。

本番確認とお知らせ

独立担当がデプロイ対象コミットと全ジョブ成功を照合したうえで、本番配信のUIに合成GET応答を渡して確認した。同じ形式の6ケースで初期生成・再生成・実スコアの定期取得更新の枠寸法と操作位置が一致し、画像共有の対応/非対応を含む最下部8ケースでもスクロール位置の変化は0だった。生成中の共有表示は保たれ、無効なボタンを強制クリックしても共有処理は発火せず、古いダウンロードリンクもなかった。

生成失敗・再試行・QRリンク欠落・デコード前の固定枠も確認した。アプリ内のお知らせ 2026-10-25-result-preview-layout は10言語で反映され、閉じる操作と既読後の抑止も確認できた。

独立確認の結果。この検証は本番配信コードと合成データを組み合わせたもので、非GETや他のAPI通信は遮断し、DB操作は行っていない。共有APIもモックであり、実端末の共有先選択やSNSへの実投稿は検証していない。

最近の修正の本番反映と、お知らせとの差分を確認

releaseリリース済み

最近の修正の本番反映と、お知らせとの差分を確認

リリース状況

対象の修正は本番リリース済み。 2026-10-25に調査し、PR #54のマージコミット cb5ff5a4b4244917613778b25b2e2518eceb2edb に対する本番デプロイrun 40070115937 と、その後のニュース追加コミット 00ace39c7e56b23cbd27d789195f8b5bb120d850 に対するrun 40084971217 の成功を確認した。

これは既存修正のリリース証跡の照合記録であり、全機能や実決済を今回あらためて実操作したという意味ではない。SNS画像共有は別の変更として記録する。

何を変えたか

PR #54では、ラウンドの二重登録防止、旧ポーカーの重複精算・二重返金防止、大会終了後のリエントリー・チップ調整の拒否、ゲーム作成入力の検証、麻雀カスタムオカの説明を追加した。ニュース日付の完全一致・実在日検証と、支援決済の非同期確定処理も修正した。

PR #44では、7言語・28ページのゲーム紹介本文を翻訳し、旧FAQのURLをヘルプへ転送した。

なぜ確認したか

ユーザーから、最近の機能変更のうち未告知の内容を調べ、リリース後のお知らせにまとめる依頼があった。アプリ内の最新お知らせは2026-10-09の権限別共有リンクだったため、その日以降のソース差分、マージ済みPR、本番デプロイ成功を照合した。

ユーザーにとって何が良くなったか

同じ記録・精算要求が重なった場合の整合性を改善し、終了した大会への操作を防ぐ。カスタムオカが追加ボーナスとして扱われ、精算合計が0にならないことがある点を利用者が画面上で理解できる。各言語で紹介ページを読め、古いFAQリンクからもヘルプを開ける。

技術的な内容

旧ポーカーの精算は、認証・賭け履歴・残高・ラウンド保存を同じFirestoreトランザクションで扱い、BET単位の精算印を用いる。ラウンド番号の重複もトランザクション内で確認する。競合時に再実行される読み取り範囲へ、認証資格の有効性も含める。

ハマったこと・学んだこと

PR本文の「未リリース」という文章は作成時点の記録であり、現在のリリース状態とは一致しない場合がある。反対にマージ済みという事実だけでも本番反映は保証できない。お知らせへ「使えるようになった」と書くには、マージ先と対象SHAに対応するデプロイ結果を確認する必要がある。

アプリ内お知らせは最新1件だけを表示するため、未告知の修正を個別に追加しても過去の複数件を順番に読めるわけではない。今回の新機能公開後の最新お知らせへ、利用者に影響する既存修正を簡潔にまとめる方針とした。CIや内部監査の変更は利用者向けの新機能と分けて扱う。

調査後のお知らせ公開

2026-10-25、PR #56 の本番反映後、新機能と未告知の修正をまとめた 2026-10-25-social-result-cards を全10言語で公開した。最終自動デプロイ と独立本番確認 は成功。告知には二重登録・ポーカー精算、カスタムオカの説明、多言語紹介・旧FAQ導線を含め、内部CIの変更を利用者向けの新機能としては載せていない。

既存Linuxホストを使い、GitHub Actions全ジョブをセルフホストに移行した

ciリリース済み

既存Linuxホストを使い、GitHub Actions全ジョブをセルフホストに移行した

リリース状況

リリース済み。 2026-10-24 13:50 JSTにGitHub Actionsの本番デプロイRun #40062858411がSuccessで完了した。品質チェックの2ジョブとbuild_and_deployの3ジョブすべてが成功。Firebaseへのデプロイワークフロー完了を確認したもので、公開ページの個別HTTP疎通や全機能の実ブラウザ確認まで保証するものではない。

対応したPRは #53、マージコミットは 1cc82cc75f26f63d8b747cb5ec888cc8abb228d1。PRの最新コミットに対する Quality Gate の2ジョブは成功した。

何を変えたか

3つのワークフローにある合計4実行ジョブをubuntu-latestから[self-hosted, Linux, X64]へ移行した。本番向けFirebase Hosting/Functions/Firestoreデプロイ、静的検査・自動テスト・Next.jsビルド、Firestoreルール検証、手動の依存関係修復が対象。

すでに別リポジトリで使っていたLinuxホストのRunnerプログラムを再利用し、リポジトリ単位の別Runnerとして登録した。OSユーザーと作業ディレクトリも分離し、既存のRunnerは維持した。

Firebaseデプロイが書き出すサービスアカウントJSONについて、ジョブ終了時の削除処理を追加した。セキュリティパッチ用の一時ワークフローは作業後に削除し、恒久的な依存関係修復ワークフローは意図しない自動実行を防ぐため手動実行専用にした。

なぜ変えたか

GitHubが用意するホスト型VMではなく、すでに運用中のLinuxマシンをCI/CDの実行先として利用したかったため。実行環境を自分で維持できる一方、ストレージ、ネットワーク、ポート、認証ファイル、サービスの自動起動を責任を持って管理する必要がある。

ユーザーにとって何が良くなるか

管理可能な既存ホストで全ワークフローを実行でき、環境とキャッシュを自分で運用できる。効果は実測が必要であり、1つのRunnerプロセスは通常1ジョブずつ処理するので待機が増える可能性もある。

技術的な内容

  • actions/setup-node@v4でNode.js 20、actions/setup-java@v4でTemurin Java 21をジョブごとにセットアップした。ホストへの追加手動インストールは不要だった。
  • Node.js 20.20.2/npm 10.8.2でクリーンインストール、監査、Lint、テスト、Next.js本番ビルドの成功を確認した。
  • firebase.rules-test.jsonのFirestore Emulatorの固定ポートを8080から18080へ変更した。
  • package-lock.jsonに含まれる間接依存undiciを7.30.0から7.30.1へ更新した。導入経路は開発依存のjsdomだった。
  • 修正済みのundiciでnpm run audit:production、npm run audit:dependencies、Lint、テスト、ビルドが通った。
  • デプロイ用JSONをジョブの終了処理で削除する。ただし、停電や強制終了時の完全な消去は保証しない。

Before / After

Before

3ワークフロー4ジョブすべてがGitHubホストのubuntu-latestで実行され、他のセルフホストランナーとは独立していた。

After

全実行ジョブが同じLinuxマシン上の専用Runner登録を使用する。再利用可能ワークフローを呼ぶqualityジョブはruns-onを持たず、呼び出し先にあるverifyとfirestore-rulesが実行環境を指定する。

判断とトレードオフ

  • 別リポジトリのリポジトリ単位Runnerをそのまま共有することはできないため、Runnerのアーカイブを再利用しつつ、新しいリポジトリ専用の登録を作成した。
  • 同一Linux OSユーザーで2つのRunnerを動かすと作業領域・機密ファイルを相互に読める可能性がある。権限分離のため専用OSユーザーを作成した。ただし同一ホストなので完全な仮想化隔離ではない。
  • セキュリティ修復ワークフローをworkflow_dispatchのみとした。CI設定のマージで勝手に依存関係を書き換えないため。
  • Firebaseの認証情報はGitHub Secretsから都度取得する設計を維持し、ログや記事に実際のトークンやキーを記録しない。

ハマったこと・学んだこと

1. セルフホストでポート8080が先に使われていた

firestore-rulesはJavaのインストールまで成功したのに、エミュレータの起動がCould not start Firestore Emulator, port takenで失敗した。ホストに他の用途がある場合、開発環境の典型的な8080は空いている前提にできない。既存プロセスを止めず、テスト専用の設定ファイルにあるポートを18080へ変更して解消した。Firestoreの検証スクリプトはFIRESTORE_EMULATOR_HOSTを読み、固定ポートへの依存がなかった。

2. 移行テストで、別件の間接依存脆弱性を発見

本番依存の監査は0件だったが、開発依存を含めると[email protected]にHighの検出があった。対象はHTTP応答の処理によるDoSのほか、複数のセキュリティアドバイザリー。公式の修正済み7.30.1へlockfileを更新した。テストを回避するために監査の例外を無制限に広げるのではなく、実際の依存パッケージを修正した。

3. 永続Runnerへの安全な書き込みとGitHub Actionsの認証

修復用ワークフローでは依存監査・Lint・テスト・ビルドを成功させた後、git fetchを認証なしで実行して失敗した。 actions/checkoutのpersist-credentials: falseは認証情報を残さない利点がある一方、Git操作では必要時だけトークンを明示する必要がある。読み取り・書き込み両方に一時的なHTTP認証ヘッダを与えて修正し、修正lockfileのコミットに成功した。作業用ワークフローは削除した。

検証の根拠と制限

GitHub Actions の全ジョブを既存 Linux ホストへ移行するための設計

ci未リリース

GitHub Actions の全ジョブを既存 Linux ホストへ移行するための設計

リリース状況

未マージ・未リリース。 game-score-manager の PR #53 で設定変更を作成した。2026-10-24時点で新しいランナーの登録、セルフホスト上のジョブ成功、本番デプロイは未確認。プルリクエストをマージする前に、新しいランナーを登録してPRのチェックを通す必要がある。

何を変えたか

3ワークフローファイルにある計4ジョブを、ubuntu-latest から [self-hosted, Linux, X64] へ切り替える変更を実装した。対象は本番デプロイ、通常のLint・テスト・ビルド、Firestoreセキュリティルールの検証、依存関係修復ワークフロー。

Firebaseへのデプロイ時に書き出す一時的なサービスアカウントJSONを、ジョブが成功・失敗した場合にも削除するクリーンアップ処理を追加した。ホスト準備、ランナー登録、セキュリティ、検証、切り戻しの手順を製品リポジトリに文書化した。

なぜ変えたか

既存の別プロジェクトではLinux x64のセルフホストランナーを利用しており、同じマシンを実行基盤として利用したかったため。GitHubホスト環境を使い続けるジョブを取り残さないよう、再利用可能ワークフローの呼び出し先や手動実行のワークフローも調査した。

ユーザーにとって何が良くなるか

正常に移行できれば、テスト・デプロイを管理可能な既存Linuxホストに集約できる。ホストに蓄積されたツールキャッシュを活用できる可能性がある。反面、ホスト停止や容量不足の影響を受け、プロジェクト間のジョブも待機しうるため、無条件に高速化や可用性向上を保証する変更ではない。

技術的な内容

各ジョブのruns-onを[self-hosted, Linux, X64]に変更する。再利用可能ワークフローは呼び出し側のusesジョブに実行機を割り当てず、呼び出し先にある個別ジョブのruns-onを変更する必要がある。

JavaScript/Next.js系の処理にはactions/setup-node@v4でNode.js 20をセットアップする。Firestoreの検証にはactions/setup-java@v4でJava 21をセットアップする。Firebase CLIはジョブ内でnpmからインストールする。そのため、ホスト側に恒久的なNode.js 20やJava 21のインストールは原則不要だが、Git・基本的なアーカイブ/ダウンロードコマンド、ネットワーク接続、空き容量と権限の用意が必要。

Before / After

Before

すべての実行ジョブがubuntu-latestを使用する。デプロイ環境は毎回GitHubに用意されるクリーンなVMで、ビルド終了後のファイルは継続的に残らないことを前提とできる。

After(PRマージとホスト登録後に期待する状態)

すべての実行ジョブがLinux x64セルフホストランナーを選択する。個人アカウント配下でリポジトリ単位に登録されたランナーは別リポジトリから使えないため、同じホスト上に専用のランナー登録をもう1つ作る。同一マシンを共有することと、同一GitHub Runner登録を共有することは別の操作である。

判断したこと・トレードオフ

  • ホスト登録前の本番移行はジョブが実行待ちになるため、移行設定をPRに留め、ランナーオンライン状態とPRの必須チェック成功後にマージする。
  • 独自ラベルではなく、すでに運用されているLinux x64の既定ラベルに合わせる。
  • 長期稼働するセルフホストのワークスペースにはシークレットを書き出したファイルが残り得るため、ジョブ末尾にif: always()付きの削除処理を追加する。ただし強制終了・停電まで保証できるわけではない。
  • 複数ランナープロセスを同じOSユーザーで動かすと、別々の作業ディレクトリでも相互に読み取られる可能性がある。専用OSユーザーやVM等の隔離が必要。
  • 1つのランナープロセスは通常1ジョブずつ処理するため、品質ゲート内の並列ジョブはキュー待ちが生じ得る。

ハマったこと・学んだこと

最も重要なのは、GitHub Actionsのruns-onを同じ値にすれば、別リポジトリのランナーを自動的に共有できるわけではない点。リポジトリ単位のRunner登録にはスコープがあり、個人アカウントでは組織ランナーと同じ共有設定にできない。追加のリポジトリ登録が必要である。

次に、GitHubホストの一時VMでは無視できた後片付けが、永続ホストではセキュリティ上の要件になる。シークレット値をファイルに出力する処理がある場合、always()での削除に加えて、ホスト権限の分離、故障時の残留ファイル対策が必要である。

検証とファクト確認用メモ

  • PR #53 の変更差分に、対象の3ワークフロー・4ジョブと運用ガイドが含まれることを確認。
  • 現在のarticles側のワークフロー設定が[self-hosted, Linux, X64]であることを確認。
  • game-score-managerの新ランナーのOnline状態とCI成功は未確認。
  • game-score-managerの既存の失敗runについて、今回の移行で原因が解消すると判断する根拠はない。ジョブログに基づく原因調査は別途必要。
  • 公式ドキュメント: Adding self-hosted runners、Self-hosted runners、Secure use。

記事の日付と翻訳更新日が同じになると失敗するサイトマップテスト

test未リリース

記事の日付と翻訳更新日が同じになると失敗するサイトマップテスト

リリース状況

未リリース・最終CI未検証。 修正はgame-score-managerのmasterへコミット済みだが、対応するActions実行39941776214は実行マシンが割り当てられないまま終了し、テストは開始されていない。実装したことと検証成功・本番反映は区別する。停止理由の詳細までは取得できていない。

何を変えたか

tests/app/metadata.test.tsで、翻訳済み固定ページ77件の最終更新日を検証する対象を固定ページだけに限定した。ニュースの各記事が固有の公開日をサイトマップへ反映していることは、別のテストで全公開記事について照合する。

なぜ変えたか

10月14日のニュースを補完した結果、同じ10月14日を翻訳更新日として使う既存テストが失敗した。期待値は翻訳を更新した固定ページ77件だが、実際の検証はサイトマップ全体を日付だけで絞っていた。そのため正しい日付を持つニュースが混入し、78件となった。

技術的な内容

失敗した実行39939809388では監査・lint・ニュース生成処理は成功し、893テスト中892テストが成功した。唯一の失敗は固定ページの更新日を検証するテストで、差分は新しい10月14日のニュースURLだった。ニュースの日付やサイトマップ本体に不正があるわけではなかった。

修正ではニュースを除いたstaticEntriesを作り、更新対象の固定ページのURL集合と更新日時を従来どおり厳密に比較する。同時に、ニュース記事数とサイトマップのニュース件数が一致すること、各slugに対応するlastModifiedが記事dateと一致することを追加で検証する。

判断したこと・トレードオフ

テストを削除したり期待する件数を78へ変更したりせず、検証している仕様の境界を正した。記事の公開日を別日に変更してテストを通す方法も採用しない。固定ページの翻訳更新とニュース公開は独立した出来事であり、同じ日付になることは正常である。

ハマったこと・学んだこと

日付を識別子のように使うテストは、後から正しいデータを追加しただけでも壊れる。集合を比較するときは、日付だけでなく対象となるページの種類を先に限定する必要がある。

今後の確認

実行マシンの割り当てが回復した環境で、generate:news、lint、test:run、build、デプロイと本文照合を再実施するまでは、この変更を検証済み・リリース済みとして紹介しない。今回の最終実行はrunner_idが0で全ステップが空だったため、テスト修正が失敗したと断定することもできない。

依存関係の監査を維持したままニュース公開の停止を解消

securityリリース済み

依存関係の監査を維持したままニュース公開の停止を解消

リリース状況

リリース済み。 Next.jsとVitestの依存関係修正はmasterへ反映し、2026年10月23日のFirebase Hosting・Functionsへのデプロイ成功を確認した。上記デプロイ実行では、再利用可能なQuality Gateの監査・lint・テスト・ビルド・Firestoreルール検証に加え、本番記事の本文照合も成功した。プロダクト全体に脆弱性がないと保証するものではない。

何を変えたか

Next.jsと対応するeslint-config-nextを15.5.28へ更新した。テスト実行系のvitestと@vitest/coverage-v8も4.1.12へ揃えた。package.jsonだけでなくpackage-lock.jsonもnpmで解決し直し、実際にインストールされる依存関係を更新した。Next.js配下のsharpは0.35.5であることも確認した。

本番依存関係のnpm audit結果は0件となった。開発依存関係には、修正版が未提供のbracesに関する既存の期限付き例外が残る。新しい例外の追加、監査の無効化、重大度閾値の引き下げは行っていない。

なぜ変えたか

ニュースを追加するコミットは成功していたが、デプロイ前の本番依存関係監査がNext.jsの重大なアドバイザリを検出して停止した。記事の生成失敗ではなく、公開パイプラインの安全性チェックが意図どおりデプロイを阻止していた。

Next.js更新後には本番依存関係の監査を通過した一方、全依存関係の監査でVitestの新しい問題が判明した。本番依存だけ直して終了せず、開発用の実行系も修正対象に含めた。

ユーザーにとって何が良くなったか

脆弱性が指摘された依存関係を残して公開を強行することなく、記事の公開処理を再開できた。原稿のコミット成功と本番公開成功を分け、公開できない状態を公開済みとして扱わない運用も維持した。

技術的な内容

修正バージョンを明示したnpm installで依存関係とロックファイルを更新し、npm ciで再現性を検証した。その後、既存のaudit:production、audit:dependencies、lint、test:run、buildを順に実行した。検証前の依存関係更新だけではリリース完了としない。

修正処理は一度限りのGitHub Actionsワークフローで実行した。定期スケジュールは追加していない。検証中にmasterが進んだ場合は古い状態から上書きせず、明示的に失敗させる確認も設けた。コミット対象はpackage.jsonとpackage-lock.jsonに限定した。

判断したこと・トレードオフ

セキュリティ修正のためにメジャーバージョンを一括更新するのではなく、確認済みの修正パッチへ揃えた。npm audit fix --forceは使用しなかった。ロックファイルを手で置換せず、依存関係解決とテストの両方を通して整合性を確かめた。

既存のbraces例外は開発用の限定された依存経路と対象アドバイザリだけに適用され、再確認期限は2026年11月8日である。これは修正済みを意味しないため、今後の修正版提供と依存経路の更新を追跡する必要がある。

ハマったこと・学んだこと

最初に試したnpm audit fix --package-lock-onlyは、npm内部でnullのedgesOutを参照するエラーとなった。監査結果を握りつぶして先へ進めるのではなく、Next.js、eslint-config-next、Vitest、coverageパッケージの修正バージョンを明示して更新し、変更していない監査ポリシーで再検証した。

本番依存の監査成功と、開発用ツールを含む監査成功は別である。また、GitHub Actionsのトークンで作成されたコミットでは後続ワークフローの起動に制限があるため、修正コミットの存在だけでデプロイ開始を推測せず、実際の実行と本番反映を確認した。

ニュース公開の確認をタイトル検索から本文全体の照合へ変更

ciリリース済み

ニュース公開の確認をタイトル検索から本文全体の照合へ変更

リリース状況

リリース済み。 2026年10月23日、変更を含むFirebaseデプロイと「Verify every published news article on production」の成功を確認した。本番一覧・詳細のHTTPステータス、可視タイトル、記事本文、見出し、リンク先を各公開記事について照合する処理がデプロイ後に実行されている。

何を変えたか

scripts/verify-news-production.mjsと回帰テストを追加した。デプロイ後の「最新記事タイトルがHTML中に含まれるか」という確認を、生成済みニュースデータの公開記事すべてに対する照合へ置き換えた。結果はnews-production-verification.jsonに出力し、GitHub Actionsの成果物として保持する。

なぜ変えたか

最新記事だけを確認する方法では、過去の日付で補完した記事が本番へ反映されたかを検証できない。HTML全体の文字列検索には、本文がなくてもスクリプト内のデータやメタ情報にタイトルがあれば成功してしまう問題もある。HTTP 200、タイトル表示、全本文反映をそれぞれ独立した条件として扱う必要があった。

ユーザーにとって何が良くなったか

コミットやビルド成功だけで「記事を公開した」と報告するのではなく、読者が実際に読むページの内容まで確認できるようになった。未公開分を後から追加した場合にも、最新日付の記事だけが確認対象になる取りこぼしを防げる。

技術的な内容

生成処理が出力するsrc/generated/news-content.jsonのpublished: true記事を読み込む。JSDOMで本番HTMLを構造として解析し、一覧には同じサイト内の正しい詳細URLを持つリンクが存在すること、詳細にはarticle h1とarticle .prose-newsが存在することを確認する。

コミットされたMarkdownを、アプリと同じ方針でHTMLへ変換して期待値を作る。Unicode正規化と空白の正規化を行い、スクリプト・スタイル・明示的な非表示要素を除外した本文のテキスト全体を比較する。見出しの順序とリンク先の集合も別途比較するため、見出しだけ同じ古い段落や、表示テキストだけ同じで出典URLが異なるページも失敗となる。

一覧と詳細のHTTPステータスは両方とも200を必須とする。キャッシュ対策の照会値、通信タイムアウト、有限回の再試行を設け、確認できない場合には成功として終了しない。

Before / After

変更前は最新のファイル名を選び、取得したHTMLへタイトル文字列が含まれるかを主に確認していた。変更後は全公開記事を対象に、一覧リンク・本文中のタイトル・本文全体・見出し・リンクを確認し、検証対象コミットと実施時刻をJSONへ残す。

判断したこと・トレードオフ

DOMを解析するために既存の開発依存であるJSDOMを使用した。単純なgrepより処理と依存は増えるが、本文と埋め込みデータを区別できる。期待値のMarkdown変換には実際のレンダリング方針を再現し、回帰テストではアプリのレンダラーを通したHTMLと照合して差異を検出する。

これは本番HTMLの構造・本文照合であり、ブラウザの画素単位の見た目、すべてのCSSによる表示制御、外部出典の内容そのものの真偽まで保証する検査ではない。記事のファクトチェックは別工程として必要である。

ハマったこと・学んだこと

記事の存在確認だけでは本文の完全性を保証できない。タイトルがスクリプトにだけ存在する場合、段落が欠ける場合、段落が古い場合、出典URLだけが違う場合、別ドメインへのリンクを一覧に置いた場合などを回帰テストに含めた。

復旧中は通常の公開処理からも新しいコミットが入る。ブランチ参照を更新するときは期待する先頭コミットを指定し、競合したら最新状態を読み直してから変更を載せ直す。古いツリーを強制的に反映して、並行して追加された記事を消す方法は採用しなかった。

既存ゲームのURLを維持し、新規ゲームだけ権限リンク方式にする

improvement未リリース

既存ゲームのURLを維持し、新規ゲームだけ権限リンク方式にする

リリース状況

未マージ・未リリース。 ローカル実装・必須自動検証・独立レビュー・実ブラウザ確認が完了している。本番へのデプロイや既存ゲームのデータ更新は行っていない。

何を変えたか

PR #37の認可方式を、ゲーム本体の accessVersion で分ける設計へ変更した。フィールドがない既存ゲームは従来のURLを知る人が記録・管理でき、新しく作るゲームは数値2を保存して管理・編集・閲覧のキーを検証する。

なぜ変えたか

最初の案は全ゲームへ権限キーを要求し、既存ゲームの管理者確認・キー発行・配布を公開前の条件としていた。しかし、既存利用者が突然ゲームを開けなくなることと、アカウントを持たない利用者へキーを引き渡す運用が未整備であることが問題だった。既存URLの所持者をゲーム権限上の管理者として扱う互換方針を明示的に選択した。

ユーザーにとって何が良くなったか

既存ゲームでは再招待やキー入力を要求せず、保存済みの履歴や従来URLから利用を継続できる。新規ゲームは作成者が管理用キーを持ち、参加者へ編集用リンク、結果を見せたい相手へ閲覧用リンクを共有する。

技術的な内容

  • ゲーム本体の accessVersion と、秘密ハッシュ記録の内部形式 gameAccess.version を別の概念として扱う。
  • 新規作成ではサーバーがバージョン2と3役割のキー記録を原子的に作成する。クライアントが従来方式を選択することはできない。
  • キー記録の有無だけで旧方式を判定しない。新方式のゲームで記録が失われても、認可なしの管理へ戻さない。
  • サーバーが判定したアクセス方式をAPIから画面へ伝え、既存ゲームの通常URL共有と新規ゲームの権限リンク共有を分ける。
  • ポーカーの端末本人確認・ディーラー判定・既存大会の管理キー確認は、ゲームの権限とは別に維持する。
  • ブラウザからFirestoreへの直接アクセスは拒否したまま、既存ゲームもAPI経由で利用する。

Before / After

Before

PR #37の初期案では、権限記録のない既存ゲームを拒否し、運用者による事前移行とキー配布を必要としていた。これは本番へ公開された仕様ではない。

After

既存ゲームは従来方式、新しい作成APIで作るゲームは権限リンク方式とする。必須移行のツールと手順を撤去し、既存ゲームを新方式へ切り替える追加機能は今回提供しない。

判断したこと・トレードオフ

既存URLを知る人は引き続きゲームを管理できる。既存ゲームにも新しい3役割の制限が適用されるとは説明しない。ゲームの作成時刻で区切る方式や、旧ゲームIDを本番で全件登録する方式は採用しなかった。

バージョンと権限記録を管理者が両方削除した状態は、元からバージョンのない旧データと区別できない。通常APIからバージョンを変更させず、直接クライアントアクセスを拒否する境界を維持する。未知のバージョン、権限記録だけが残る不整合、DB障害は従来方式として許可しない。

ハマったこと・学んだこと

認可を強化するだけでは、既存ユーザーが変更後も利用できるとは限らない。ログインのない共有サービスでは、所有者の本人確認を前提とする移行手順を後から追加しても、必要な本人確認情報がそもそも保存されていない。

また、キーが見つからない状態を既存データと同一視すると、新方式のデータ破損が権限の解除につながる。アクセス方式を示すマーカーをデータ本体へ明示し、認証情報とは独立して判定する必要がある。

検証

Node.js 20でLint、110ファイル・858テスト、本番ビルド、依存監査が成功した。Firestore Emulatorの匿名・認証済みクライアントによる直接アクセス拒否32件も成功した。ローカルサーバーをビルドと並行起動して出力が競合した失敗があり、停止後に単独のビルドを再実行して成功を確認した。

実APIでは旧ゲームのキーなし取得・メンバー追加・スコア保存・管理を確認した。新規ゲームはバージョン2で作成され、キーなしでは拒否される。新規ゲームの権限記録だけを削除した場合も、キーの有無にかかわらず拒否されることを確認した。これらはdemoプロジェクトのエミュレーター上の結果であり、本番のユーザーデータを使用していない。

独立担当は7ファイル・172テストを実行して成功を確認した。通常・ゼロサム・麻雀・キャッシュポーカー・大会の旧ゲームを、キーを持たない別ブラウザ状態で従来URLから開き、参加・記録・管理・同期と通常URL共有を確認した。旧大会では保存済み管理キーの正誤と、キーがない端末の案内も確認した。

新規ゲームは全5モードを画面から作成し、バージョン2の保存と編集者・閲覧者の操作制限を確認した。共有リンクの失効、同一タブでの新招待の取り込み、管理者権限の維持、別ブラウザ状態での管理用復旧リンクも検証した。独立レビューで必須の未解決指摘はない。

既存リンクを維持する権限別共有リンク機能のリリース

releaseリリース済み

既存リンクを維持する権限別共有リンク機能のリリース

リリース状況

2026-10-09 本番リリース済み。 PR #37と、本番確認で必要になった修正PR #39・#40をマージした。最終コミット 3ce88d2302587b304370f884aa3c75393b9aee28 の自動デプロイが成功し、独立担当による本番最終確認も合格した。アプリ内の新機能お知らせも公開・表示確認済み。

何を変えたか

新しく作るゲームで、管理・編集・閲覧の権限を共有リンクごとに分けられるようにした。既存ゲームは従来のリンクを維持し、再招待やキー配布を要求しない。

なぜ変えたか

スコアを記録する参加者と、結果を見る参加者で操作権限を分けたい一方、利用中のゲームが更新によって開けなくなることを避けるため。

ユーザーにとって何が良くなったか

新規ゲームは共有先の用途に応じたリンクを配れる。既存ゲームは今までのリンクを使い続けられる。ポーカーの本人確認や旧大会の管理端末の条件は維持する。

技術的な内容

ゲーム本体のアクセスバージョンで認可方式を判定する。新方式の認証記録が欠落しても旧方式へ降格させない。Firestoreの直接アクセスを閉じ、サーバーAPIで旧新両方式を扱う。

ハマったこと・学んだこと

既存リンク互換を本番で確認するには、更新前に旧方式の検証用ゲームを作っておくと、同じリンク・履歴を更新後に継続利用できることを検証できる。一般利用者のゲームを変更せずに互換性を確認できる。

ローカルでAPIが no-store を返していても、本番のHosting全体への公開キャッシュ指定が上書きすることがある。初回本番確認でその不一致を検出し、静的なNext.jsビルド成果物だけを長期キャッシュ対象にする修正を別PRへ分けた。権限を変えた応答の越権再利用は観測していないが、キー失効や同期を含む仕様を満たすため公開キャッシュを除去する。

更新前の作成画面がブラウザーに残っている場合、その画面は新規作成APIから返された権限キーを保存できない。旧方式での新規作成へ戻すのではなく、新しいクライアントの対応をヘッダーで確認し、未対応なら保存前に再読み込みを案内する。

ポーカー操作ではAPIの例外処理では捕捉できない503も起きた。Functionsログとrevision設定を照合すると、現行・直前とも256MiBのメモリ上限であり、コンテナーのメモリ超過停止が確認できた。必要メモリだけを512MiBに明示する修正を行い、他の常時稼働設定は変更しない。

さらに、正規ドメインの実ブラウザーではCloudflareの自動解析スクリプトがゲーム画面にも注入されていた。ソースコードの解析タグを除外するだけでなく、中継側がHTMLを変更することも考慮する必要がある。ゲーム画面への注入抑止と、外部スクリプトの実行を拒否するヘッダーを追加した。キー送信は観測していない。

主要操作が成功していても、バックグラウンドの状態取得が成功しているとは限らない。本番検証用ゲームのポーカー状態取得の500をログで検出し、必要な pokerActions のインデックスが本番にないと判明した。集計処理と定義は今回の権限リンク追加前から存在したが、デプロイ手順がルールだけを適用していた。既存の本番インデックスを保持したうえで、インデックスもリリース工程に含めた。

本番再確認

PR #39の自動再デプロイは成功し、実際のCloud Run revisionが512MiBになったことを確認した。更新前に作った検証用ゲームの同じURLで、正規ドメインとFirebase直の双方のAPIが no-cache, no-store, must-revalidate を返す。新規ゲームは管理キーありで200、キーなしで401、未対応の古い作成クライアントは426で拒否される。

初回に失敗した旧キャッシュポーカーのハンド開始と本人のcall、旧大会の開始は再反映後に成功した。旧大会の管理キーがない端末では従来の案内を維持する。

新規ゲームは通常・ゼロサム・麻雀・キャッシュポーカー・大会の全5モードを本番画面から作成し、主要操作を確認した。キャッシュポーカーは管理者と編集者の別端末参加・ハンド開始・同期、大会は管理者の開始と閲覧者によるタイマー閲覧、管理操作が表示されないことを確認した。

最終反映後、ゲームへの直アクセスとホームからの新規作成、別端末での共有リンク利用で、外部スクリプトがないことを確認した。QR表示も正常。ポーカー用インデックスと既存の履歴用インデックスが利用可能になり、旧・新キャッシュポーカーの状態取得、総バイイン・収支表示が成功した。

管理・編集・閲覧の読み取り、閲覧者の書き込み拒否、キーなしの拒否、古い作成画面の再読み込み案内、編集者の記録と管理者への同期を再確認した。共有リンクの再発行による旧リンク失効、同じタブでの復旧、管理者権限の保持、別端末での復旧も確認済み。最終の自動検証は112ファイル・869テストとビルドが成功し、独立レビューに必須の未解決指摘はない。

ポーカーのハンド結果と脱落順位を同じトランザクションで確定する

bugfix未リリース

ポーカーのハンド結果と脱落順位を同じトランザクションで確定する

リリース状況

未マージ・未リリース。 Issue #32への修正をPR #35に追加した。2026-10-08の確認時点でPRはopen、マージされていない。本番のデータ更新やデプロイは実施していない。

本記録はPRのコミット e932b319eba11f589b7b6be3bdbbdf4684592490 に対応する。以下は実装とテストで確認した内容であり、本番反映済みという意味ではない。

GitHub Actions run 37729347931 は成功した。

何を変えたか

ポーカーのハンド確定時に、チップ残高・脱落時刻と順序・ラウンド履歴・進行中ハンドの削除を同じFirestoreトランザクションへまとめた。トランザクションは、関連する読み書きをまとめ、すべて成功するか、変更を適用しないかのどちらかにする仕組みである。

対象は、フォールドにより勝者が1人に決まる自動確定と、ショーダウンで勝者を指定する確定の両方。確定後に別途実行していた脱落同期をなくし、同期や保存が失敗した場合に同じハンドを再試行できるようにした。

同じハンドで複数人が脱落した場合の順序も、明示的な条件で固定した。

なぜ変えたか

従来はハンドの結果をトランザクションで保存した後、別の処理で脱落順位を同期していた。後段の同期だけが失敗すると、チップとラウンドは確定済みなのに順位は更新されず、APIはエラーを返していた。

その時点で進行中ハンドも削除済みだったため、利用者が同じ操作を再試行しても、元のハンドからやり直せなかった。1つの操作に見える処理の途中に、別々の確定点があったことが原因だった。

本番反映後に期待する改善

チップが0になっているのに脱落記録がない、保存結果は確定したのに利用者には失敗と見える、といった部分成功を防ぐ。保存失敗時はハンドが残るので、再試行でチップと順位をまとめて確定できる。

同じ進行中ハンドへの確定要求が重なっても、ラウンドや順位の登録は1回だけになる。確定後の重複要求は進行中ハンドがないことを確認して拒否する。新しいハンドをまたぐ一般的なHTTP要求の冪等性まで追加した変更ではない。

技術的な内容

確定後のチップを計算してから、関連情報を書き込む

submitPokerAction と awardShowdown で、各プレイヤーの開始時チップ数、確定後のチップ数、席番号を先に計算する。ラウンド番号などの必要な情報も読み終えてから、脱落同期と各書き込みを行う。

syncTournamentEliminations は、計算済みの結果と既存の脱落記録から更新内容を決める。トランザクション外からの呼び出しは拒否する。また、大会モードなのに同期処理が設定されていない場合も、ハンドだけを確定せずエラーにする。

Firestoreでは「同期関数を内側へ移す」だけでは足りない

Firestoreのトランザクションは、書き込みを始める前に読み取りを済ませる必要がある。既存のチップ書き込みの後へ同期関数を移すだけでは、同期関数内の再取得が書き込み後の読み取りになってしまう。

さらに、トランザクション内の読み取りは、そのトランザクションへ追加した書き込みを参照しない。今回の処理では、更新前のプレイヤーデータとハンドの計算結果から確定後チップを作り、それを同期処理へ渡した。データベースへの再取得を、計算済み状態を受け渡す代わりに使わない設計にした。

同時脱落の順序と、再エントリー後の採番

同時に脱落する場合は、ハンド開始時のチップ数が多いプレイヤーを上位にする。同額の場合は席番号、プレイヤーIDの昇順で表示順を固定する。データベースの取得順や配列の入力順に順位を依存させない。

内部の eliminationRank は、後に脱落した人ほど大きくなる順序番号として使っている。次の番号を現在の脱落者数だけで決めると、再エントリーで欠番ができたときに既存番号を再利用してしまう。そのため、既存番号の最大値も確認して次の番号を決める。すでに記録されている脱落時刻と順序は上書きしない。

Before / After

場面 変更前 変更後
脱落同期の失敗 チップ・履歴だけ確定し、進行中ハンドは削除済み 関連変更を確定せず、進行中ハンドを保持
失敗後の再試行 元のハンドがなく、同じ処理をやり直せない 同じハンドから再度まとめて確定
同時に届いた確定要求 後段同期も含む整合性がトランザクションで保護されていない 同じハンドを読む競合を検出し、確定を1回に限定
複数人の同時脱落 順序を明示的な業務条件で固定していない 開始時チップ数・席番号・IDで固定
再エントリー後の番号 現在の脱落者数から採番 既存番号の最大値も使い、欠番があっても重複を回避

判断したこと・トレードオフ

脱落情報は同じFirestore内にあり、ハンド確定と一緒に更新できるため、非同期ジョブや同期待ち状態を新しく導入しなかった。利用者にとって一体の操作を、同じ確定点にまとめる方法を選んだ。

一方、脱落情報の保存に失敗した場合はハンド全体を未確定にする。チップだけ先に反映させる利便性よりも、結果と順位の整合性を優先した。過去の部分成功でできたデータを本番で一括修復したという意味ではない。

ハマったこと・学んだこと

「トランザクションを使っているか」だけでなく、「利用者が1つの操作と考える変更が、どこまで同じ確定に含まれるか」を確認する必要がある。順位更新を後処理として扱うと、APIが返す成否と保存済みデータの状態がずれてしまう。

テストでは、単に更新メソッドの呼び出し回数を確認するのではなく、書き込みを保留してから確定するテスト用ストアを使った。脱落の書き込みを準備した後に例外を発生させ、チップ・順位・履歴・進行中ハンドが変更されていないことを確認した。さらに、その後の再試行と同時要求まで一連の状態変化として検証した。

検証した内容

修正PR単体に対応するローカル環境で、101テストファイル・676テストが成功した。今回追加した回帰テストは12件。

  • 自動確定・ショーダウンそれぞれで、脱落同期失敗時にすべての変更が取り消されること。
  • コミット失敗後、同じハンドを再試行して確定できること。
  • 同時要求で競合した際、再試行されても確定が1回だけになること。
  • 確定済みハンドへの再要求で、履歴と順位が二重登録されないこと。
  • 同期処理が未設定の場合に、ハンドを先に確定しないこと。
  • 同時脱落の入力順の違い、順位番号の欠番、既存脱落情報の保持。

npm run test:run は全テストを実行するコマンド。npm run lint はコードの静的検査、npx tsc --noEmit --incremental false は生成ファイルを作らないTypeScriptの型検査で、いずれも成功した。差分の空白などを調べる git diff --check も成功した。

テスト用ストアは書き込みの保留、競合による再試行、書き込み後の読み取り禁止を再現するもの。Firestore Emulatorや実サービスのロック、実端末間の通信を完全に検証したという意味ではない。前述の101ファイル・676テストは、このPR単体の実測値である。

別途、#29・#31・#32を組み合わせたローカル統合検証でも、109ファイル・824テスト、Node.js 20でのクリーンインストール・依存ツリー検査、コード検査、型検査、依存監査ゲート、本番向けビルド(241ページ)が成功した。統合検証の件数は、各PR単体の件数とは区別して記録する。

継続事項

本番反映時には、大会モードの自動確定・ショーダウン・順位表示を確認する。本番で提供できた際は、新しいリリース記録として追記する。

依存更新のリリースとハンド確定修正のマージを確認した

bugfix未リリース

依存更新のリリースとハンド確定修正のマージを確認した

リリース状況

本ファイルのstatusは、後続のハンド確定修正について本番反映の完了をまだ確認していないため unreleased とする。個別の到達点は次のとおり。

変更 マージ 本番反映
PR #36・依存更新/監査ゲート マージ済み。7940eea1bedc18b8cc195b6aabdcca9f2f50d1f4 リリース済み。Actions run 37792231192でHosting+Functionsへのデプロイと本番ニュースの確認が成功
PR #35・ハンド結果と脱落順位の同時確定 マージ済み。9034621a0d5576e6cd45fe47ab69166a75fd526c 未リリースとして管理。記録時点でActions run 37793978633のデプロイが実行中で、完了未確認

PR #36のリリース確認はデプロイジョブとその本番確認ステップに基づく。すべての画面・APIを本番で手動検証したという意味ではない。PR #37のゲーム権限導入は、この2本のマージには含めず、移行条件の確認まで保留している。

何を変えたか

修正版依存と監査ゲートを含むPR #36を、既存のレビューとCI成功を確認してmasterへマージした。その後、PR #35のブランチへ更新されたmasterを取り込み、依存更新とハンド確定修正を同時に含む状態でCIを再実行してからマージした。

PR #35の更新後headは 5ed960a3f390dfed9bc0b555e6be3b16779e8ae8。対応する Quality Gate run 37793179911 で、依存インストール、本番依存監査、限定例外付きの全依存監査、Lint、全体テスト、本番ビルドが成功した。

なぜ変えたか

別々のPRでCIが成功していても、先に依存更新をマージした後の組み合わせは別の状態になる。過去の緑色のチェックだけを根拠にせず、マージ対象のheadと取り込むmasterを明示して、更新後の依存関係でも既存の修正が通ることを確認するため。

技術的な内容

PR #36は依存関係、ワークフロー、ニュースのYAML読み取りなど11ファイルを変更する。PR #35はポーカー実装、設計書、テスト用ストアなど7ファイルを変更し、ファイルの重複がなかった。

比較で変更ファイルを確認し、masterの依存更新を残したままPR #35の7ファイルを統合した。Gitツリー上の既存blobを再利用し、テキスト全体の手作業再作成による余計な変更を避けた。統合後にmasterとの差分が元の7ファイルだけであることを確認した。

ブランチ更新とPRマージには期待するhead SHAを指定し、別の更新が割り込んだときにその変更を見ずに進めないようにした。ブランチ更新APIが一時エラーを返した際も、先に現在の参照を読み直して未更新を確認してから再試行した。エラー応答だけで未反映だと決めつけない。

ユーザーにとって何が良くなったか

依存更新は本番へ反映された。ハンド確定修正はmasterに取り込まれ、更新済み依存との組み合わせで検証済みとなった。本番への反映が確認できるまでは、ハンド結果と順位の不整合が本番でも解消したとは扱わない。

ハマったこと・学んだこと

マージ可否の確認には、ソースのレビュー、対象headのテスト結果、統合先のバージョン、デプロイの実績を分けて残す必要がある。マージ完了だけで「リリース済み」にすると、実行中や失敗したデプロイを見落とす。

依存監査の成功にも意味の区別がある。本番依存は検出0件だが、全依存には未修正のbraces由来の開発依存例外が残る。期限・依存経路を限定する監査ゲートの通過は、その脆弱性自体が修正されたことを意味しない。

アカウント不要の共有を維持しながら、ゲームごとの閲覧・編集・管理権限を分離する

security未リリース

アカウント不要の共有を維持しながら、ゲームごとの閲覧・編集・管理権限を分離する

リリース状況

未マージ・未リリース。 Issue #29に対する実装・レビュー段階の記録。本番へのアプリ反映、Firestore Security Rulesの適用、既存ゲームの移行は実施していない。以下はコード上の変更と検証内容であり、本番で利用可能になった機能の説明ではない。

実装は PR #37、本記録に対応するコミットは a9ea011718fe1fadbd0f72a7b9a18bb5eb8b329a。ローカル検証とGitHub Actionsの結果は分けて記録する。

何を変えたか

ゲームごとに閲覧用・編集用・所有者用の認証情報を発行し、APIで必要な権限を確認するようにした。ゲームIDや表示名を知っているだけでは、内容の取得や書き換えを許可しない。アカウント作成を必須にするのではなく、権限を持つ招待リンクで参加する方式を採用した。

保護対象はゲーム名の更新だけに限定しなかった。参加者、ラウンド履歴、ポーカー操作、大会管理のAPIへ共通の認可処理を適用し、サーバーで生成するHTMLやプレビュー用メタデータからもゲーム情報の取得を外した。ブラウザーからFirestoreを直接読む経路も、権限を検証するAPI経由へ移した。

既存ゲーム向けには、運用者が所有者を確認してから認証情報を初回発行する移行スクリプトを追加した。ゲームIDや記録済みのスコアは変更しない。

なぜ変えたか

従来は、ゲームのURLを知っていることと、そのゲームを管理してよいことを分離できていなかった。公開一覧を止める対応だけでは、既知のゲームIDを指定する更新APIの問題が残る。表示名も参加者が入力できるため、その文字列を所有者の証明として扱う方法では不十分だった。

APIだけを保護しても、サーバーのページ描画やブラウザーのFirestore接続が無認可のままなら、同じ情報へ別経路で到達できてしまう。読み取り・更新・画面表示・データベース接続を、同じ方針で確認する必要があった。

本番反映後に期待する改善

閲覧だけを許可する相手と、一緒に記録する相手を分けられる。編集用リンクを共有しても、ゲームの設定変更や共有リンクの再発行まで許可する必要はない。共有先を見直したいときは編集用・閲覧用の認証情報を再発行でき、以前のリンクを無効にできる。

既存のゲームは、権限移行後も同じIDと履歴を使える。ただし、認証情報のない旧URLから自動的に所有者権限を取得することはできず、確認済みの所有者への引き継ぎが必要になる。

技術的な内容

ゲームIDと権限の証明を分ける

各権限の認証情報は、Node.jsの暗号用乱数から32バイトを生成して作る。APIへは Authorization: Bearer ヘッダーで送信する。Bearerは、その秘密の値を所持していることを権限の証明として扱う方式を指す。

データベースには生の認証情報を保存せず、SHA-256で計算したハッシュ値を、公開ゲーム文書とは別の gameAccess 文書に保存する。受信した値もハッシュ化し、対象ゲームの記録と照合する。別ゲームの有効な認証情報を、そのまま他ゲームで使うことはできない。

主な権限は次のように分けた。上位権限は下位の操作も実行できる。

権限 主な許可対象
viewer ゲーム、ラウンド履歴、ポーカー状態、大会表示の閲覧
editor 閲覧に加え、参加・点数記録・ポーカー操作。既存のプレイヤー本人確認やディーラー確認も維持
owner 編集に加え、ゲーム名・麻雀設定・終了状態・参加者名の管理、編集用・閲覧用リンクの再発行

ラウンド単体のAPIは、要求されたラウンドの保存済み gameId を読み、そのゲームに対する権限を確認する。リクエスト側に「自分のゲームである」と申告させた値で認可しない。

読み取り経路と、通常の共有リンクも保護する

招待の認証情報はURLのフラグメント部分に載せ、画面側で取り込んでからURLから除去する。取り込んだ値の権限はサーバーで確認する。通常の共有操作では編集用・閲覧用リンクを使い、所有者用リンクは明示的な復旧操作に分ける。

ゲームのAPIレスポンスはキャッシュさせないヘッダーを付ける。ゲームページのサーバー描画とメタデータは、認証前にゲーム名や参加者を取得しない。Firestore Security Rulesではクライアントの直接アクセスを閉じ、サーバーの共通認可処理を通す。

ブラウザーへの保存が禁止・容量超過で失敗した場合は、現在のタブ内に認証情報を保持する。古い保存値が残っている場合も、再発行した新しい値を優先する。作成直後はページ全体を移動して、それまで公開ページで動いていたスクリプトを引き継がない。ゲームページではAnalytics・AdSenseを読み込まない。

画面も閲覧・編集・所有者の権限に合わせて操作を分ける。ポーリング中に認証が失効した場合は、ゲーム表示を閉じて権限の確認へ戻る。新規大会の管理キーは所有者の認証情報と統合し、既存大会の管理キーは維持する。

移行は「秘密を保存してから、権限を初回作成する」

scripts/migrate-game-access.mjs は、標準では状態確認だけを行う。適用時は、認証情報をリポジトリ外の所有者だけが読めるファイルへ先に保存する。0600 は、ファイルの所有者だけに読み書きを許可する権限設定である。DB更新後の応答が不明確でも、同じファイルを使って再試行できる。

Firestoreのトランザクションでは、ゲームの存在と認可文書の状態を確認する。認可文書がなければ初回作成し、同じハッシュが保存済みなら適用済みとして扱う。違う認証情報が存在するときは、所有者や共有リンクを上書きしない。既存URLを持っているだけの利用者が、自分を所有者として登録するAPIは用意しない。

判断したこと・トレードオフ

URL共有によるアカウント不要の利用方法を維持するため、ゲームごとの秘密値を権限の証明とする方式を選んだ。その分、リンクを受け取った相手が同じ権限で利用できることを前提とする。通常共有と所有者の復旧を分け、共有用の権限を再発行できる構成が必要だった。

既存ゲームの所有者を表示名や旧URLだけから安全に決めることはできない。自動で権限を昇格させず、運用者の確認を挟む移行を選んだ。これは移行作業を必要とする判断であり、旧URLがそのまま無条件で使える互換性を保証するものではない。

ハマったこと・学んだこと

認可はAPIの入口に関数を追加するだけでは完結しなかった。サーバー描画、プレビュー、Firestoreの直接接続、共有時に選ぶ認証情報など、同じデータへ到達する経路を確認する必要があった。

移行レビューでは、秘密ファイルのJSON解析エラーをそのままログへ出すと、Node.jsのエラーメッセージに入力の一部が含まれることを確認した。また、パスを文字列として絶対パス化するだけでは、親ディレクトリのシンボリックリンク経由でリポジトリ内へ保存できる。秘密値を扱うツールでは、エラー文を固定化することと、実体のパスで保存先を確認することが必要だった。

この2点は実装を修正し、JSONエラーに秘密値が含まれないこと、リポジトリ内を指すディレクトリ別名を拒否して秘密ファイルを作らないことを、回帰テストで確認した。

最初のCIでは、既存の firebase.json にある webframeworks 設定の影響で、Firestore Emulatorが起動する前に処理が停止した。Firestore Rulesの検証用に firebase.rules-test.json を追加し、検証がホスティング向け設定に依存しないよう切り替えた。

検証した内容

認可対応PR単体のローカル検証で、106テストファイル・799テストが成功した。npm run lint によるコード検査、TypeScriptの型検査、本番向けビルドも成功した。localhost上で本番向けビルドを起動し、日本語・英語の保護されたゲームページについて、HTMLとRSCの計3経路のスモークテストも成功した。RSCは、React Server Componentsがブラウザーへ渡す描画用データを指す。

認可の検証では、未認証・別ゲームの認証情報・viewerによる更新・editorによる管理操作を拒否すること、ownerに許可した操作が実行できることを確認した。共有リンクの再発行、旧ゲームの移行と再試行、保存できないブラウザーでの招待・共有もテスト対象に含めた。

独立レビューでは、移行・ブラウザー保存・招待の受け取り・共有パネルの4テストファイル、計21テストの成功を確認した。この件数は認可対応全体ではなく、該当4ファイルの部分検証を示す。

Firestore EmulatorによるRules検証は、GitHub ActionsのFirestoreジョブ で成功した。2026-10-08 05:09:12 UTCのログで、未認証・認証済みクライアントからの直接アクセスに対する32チェックの成功と、検証スクリプトの正常終了を確認した。

別途、#29・#31・#32を組み合わせたローカル統合検証でも、109ファイル・824テスト、Node.js 20でのクリーンインストール・依存ツリー検査、コード検査、型検査、依存監査ゲート、本番向けビルド(241ページ)が成功した。統合検証の件数は、各PR単体の件数とは区別して記録する。

GitHub Actions run 37730863961 は通常のverifyジョブを含めて成功した。クリーンインストール、コード検査、全体テスト、本番ビルドと、独立したFirestore Rulesジョブがすべて成功している。

本番のFirestoreデータや実利用者の認証情報を使った検証は、この更新記録の根拠に含めていない。テストの成功から、プロダクト全体の安全性や本番反映を断定しない。

継続事項

本番反映時にはアプリとSecurity Rules、既存ゲームの移行を対応付けて確認する。本番提供が確認できた際は、過去の記録を上書きせず、別のリリース記録を追加する。

本番依存の監査検出を0件にし、未修正の開発依存を期限付きで監視する

dependency未リリース

本番依存の監査検出を0件にし、未修正の開発依存を期限付きで監視する

リリース状況

未マージ・未リリース。 Issue #31に対する依存関係更新と監査方針の記録。ローカルで確認した依存関係と監査結果を示しており、本番の依存関係が更新済みという意味ではない。

実装は PR #36、本記録に対応するコミットは 588ec4d73bff79884c70cc57ae489259c09c71c6。

何を変えたか

Next.js、Firebase、Firebase Admin SDKなどを更新し、必要な間接依存には範囲を限定した overrides を設定した。overrides は、親パッケージが依存しているライブラリについて、プロジェクト側で解決するバージョンを指定するnpmの仕組みである。

本番用の依存関係を監査する処理と、開発用も含めて監査する処理を分けた。未修正版が残る開発用の braces については、対象アドバイザリー・バージョン・導入経路・開発依存であること・期限を限定した監査ゲートを追加した。すべてのHighを無視する設定にはしていない。

CIに残っていたnpmのTLS証明書検証を無効にする設定も取り除いた。

なぜ変えたか

Node.js 20の実行環境で監査したところ、古い直接依存と間接依存の両方が検出対象になっていた。直接指定したパッケージを更新するだけでは、別の親から読み込まれる古い版が残る場合がある。また、互換性を見ずに間接依存のメジャーバージョンを差し替えると、インストールは成功しても実際の呼び出しで壊れる。

一方、修正版のない開発用パッケージを含めて「監査0件」と書くこともできない。修正できた範囲、残った理由、将来の検出を止めない条件を、監査結果と一緒に残す必要があった。

監査結果

2026-10-08にNode.js 20.20.2、npm 10.8.2で確認した。数値はnpm auditが報告した影響パッケージ数であり、独立した脆弱性や攻撃可能な経路の数を意味しない。

対象 更新前 更新後
本番用依存 29件: Low 1、Moderate 15、High 8、Critical 5 0件
開発用を含む全依存 47件: Low 2、Moderate 19、High 20、Critical 6 High 5件

残ったHigh 5件は、braces の1件のアドバイザリー GHSA-vfj7-8cjw-p6xm と、その影響を受ける親パッケージの報告である。eslint-config-next がNext.jsのESLintプラグイン、fast-glob、micromatch を通して braces を利用する開発用経路に限られている。

2026-10-08のGitHub Advisory Databaseでは、braces <= 3.0.3 が対象で、修正版は記載されていない。深く入れ子になったパターンを処理する際のスタック枯渇に関する問題であり、修正済みとは扱っていない。

本番反映後に期待する改善

更新対象だった本番用依存の既知の監査検出を取り除く。開発・CIでは、既知の限定された例外を表示しながら、それ以外の新規検出や本番依存への混入を失敗として扱える。監査そのものが失敗した場合も、問題なしとして処理を続行しない。

技術的な内容

実行環境と利用APIを確認して更新する

主な更新後の直接依存は次のとおり。

パッケージ 更新後の確認版 判断・確認点
Next.js / eslint-config-next 15.5.27 既存のNext.js 15構成を維持して更新
Firebase 12.19.0 使用中のクライアントAPIとの互換性を確認
Firebase Admin SDK 13.10.0 Node.js 20を維持するため、Node.js 22を要求する14系への更新はこの変更に含めない
next-intl 4.14.9 既存の多言語構成で検証
uuid 11.1.1 間接依存側の v4() 呼び出しも確認
js-yaml 4.3.2 gray-matterからのYAML読み取り方法を明示的に変更

Next.js配下のPostCSS、Firestore配下のgRPC、特定のGoogle関連パッケージ配下のuuidなど、必要な経路だけにoverrideを限定した。プロジェクト内にある同名パッケージを、理由なくすべて同じ版へ差し替える形は避けた。

YAMLの更新は呼び出し側も変更する

gray-matterの既定のYAMLエンジンはjs-yaml 3系の safeLoad を呼ぶ。js-yaml 4系へ差し替えるだけでは、そのAPI差分が残る。そこでニュースの読み込み・インデックス生成の呼び出し箇所に、load を使うYAMLエンジンを明示した。

ニュース記事20件とREADMEの計21 Markdownについて、旧版と新版のパース結果を比較し、front matterのデータと本文が一致することを確認した。front matterは、Markdownの冒頭に置くタイトル・日付などのメタデータを指す。

例外は対象・導入経路・期限まで固定する

scripts/check-dependency-audit.mjs は、npm auditのJSON結果とlockfileを照合する。例外の再確認期限は 2026-11-08。次の条件をすべて満たす場合だけ、既知の開発用検出として通過させる。

  • 対象が GHSA-vfj7-8cjw-p6xm で、braces の版が3.0.3である。
  • 検出対象が確認済みの5パッケージと導入位置に限られる。
  • lockfile上で開発用依存になっている。
  • 親パッケージの検出理由も、許可したアドバイザリーに由来している。
  • 再確認期限を超えていない。

新しいアドバイザリー、別の導入経路、本番依存への混入、期限切れは失敗にする。JSONの破損、監査APIの失敗、監査サマリーと詳細件数の不一致も成功扱いにしない。通常のnpm auditは残る検出を報告し、ゲートも例外対象をログに示す。

判断したこと・トレードオフ

修正版のない開発依存を、根拠なく「影響なし」と結論付けることはしなかった。現時点の導入経路を限定し、期限を設けて再確認する。一方、本番用依存の監査にはその例外を適用しない。

この例外は脆弱性自体を取り除く対策ではなく、修正できない既知の検出を管理する暫定措置である。ビルド・Lintに関わる依存も開発環境で実行されるため、残存リスクと修正版の確認を継続する。

ハマったこと・学んだこと

npmのインストールが成功しても、overrideを変更した後に古いlockfileの解決結果が残り、npm ls --all が不整合を報告するケースがあった。同じmanifestから空の作業場所でlockfileを再生成し、クリーンインストール後の依存ツリーを確認した。manifest上の希望バージョンと、実際に導入された版を区別する必要がある。

間接依存の更新では、セキュリティ修正の対象版だけでなく、利用している関数と実行環境も確認する。今回のjs-yamlでは、脆弱性を減らすための差し替えと、呼び出しAPIの変更を同時に扱う必要があった。

検証した内容

確認済み: npm ci によるlockfileどおりのクリーンインストール、npm ls --all による依存ツリー検査、コード検査、型検査、監査ゲートなどの追加テスト13件、既存Markdown 21件のパース互換性確認。全体テストは 102ファイル・677テストが成功し、Next.js 15.5.27の本番向けビルドも成功した。件数は依存更新PR単体のローカル検証に対応する。

本番向けビルドを起動したHTTPスモークテストでも、日本語トップ、ニュース一覧、最新記事の詳細、同梱JPEGを使うNext.js画像最適化の4経路がすべて200を返すことを確認した。

GitHub Actions run 37730499892 も成功した。

別途、#29・#31・#32を組み合わせたローカル統合検証でも、109ファイル・824テスト、Node.js 20でのクリーンインストール・依存ツリー検査、コード検査、型検査、依存監査ゲート、本番向けビルド(241ページ)が成功した。統合環境でも本番依存の監査検出は0件で、全依存の検出は前述の限定例外に限られていた。統合検証の件数は、各PR単体の件数とは区別して記録する。

監査コマンドの役割は次のとおり。

コマンド 確認内容
npm audit --json 開発用を含む依存関係を監査し、根拠となる詳細をJSONで取得
npm audit --omit=dev --json 開発用を除いた依存関係を監査
npm run audit:dependencies 全依存の監査結果を、期限と導入経路を限定したゲートで評価
npm ls --all 実際に解決された依存ツリーに不整合がないか確認

継続事項

bracesの修正版と上流依存の更新を確認し、更新可能になった時点で例外を削除する。期限に達する前にも、本番依存への混入や検出内容の変化はCIで拒否する。本番反映は別のリリース記録で確認する。

APIと画面の権限差、および同一タブの招待更新を修正した

bugfix未リリース

APIと画面の権限差、および同一タブの招待更新を修正した

リリース状況

未マージ・未リリース。 PR #37の2件のレビュー指摘を修正し、回帰テスト、GitHub Actionsによる検証、コメントへの返信とスレッド解決まで完了した。対象headは d572f3d4ceff9e314a463c5bebc1fbcb6972f906。

本番の既存ゲームについて、権限情報の移行と所有者へのキー引き継ぎは未確認。このリポジトリではmasterへのマージから自動デプロイが起動し、権限情報のない旧ゲームはアクセス拒否になる。そのためコード上の指摘解消と本番反映可能の判断を分け、PR #37のマージを保留した。自動マージも設定していない。

何を変えたか

編集者がメンバーを追加できる画面へ揃えた

メンバー追加APIと権限表ではeditorを許可していたが、画面の追加処理、設定画面の入口、追加フォームを含む領域がowner限定だった。GameClient.tsx の436・806・831行目を canEdit に揃えた。

ゲーム名の入力と保存、麻雀設定等は引き続き canManage で制限する。「設定画面へ入れる」と「すべての管理操作を実行できる」を同一条件にしない。viewerはメンバー追加を利用できない。

同じタブに届く新しい招待を再認証する

GameAccessGate.tsx の認証effectに hashchange の購読を追加した。gameIdが変わらなくても新しい #access を取り込み、秘密値をURLから除去して認証をやり直す。ボタンによる再試行でもフラグメントを確認する。

受け取った招待は、同じ役割の古い保存値より優先する。一方、有効なownerが保存されている場合はviewerリンクで降格させない。無関係なアンカー変更では認証をやり直さない。

なぜ変えたか

権限判定をサーバーと画面の両方へ導入したとき、許可条件がずれると、APIが正しくても利用者が操作に到達できない。また、初回表示でだけ招待を読む実装では、URLのパスを変えずに招待だけ更新する操作を取りこぼしていた。

技術的な内容・設計判断

新しい招待が届くたびに認証処理の世代番号を進める。非同期処理の応答が現在の世代と一致することを確認してから、保存値の更新・削除や画面更新を行う。単に画面の表示だけを保護するのではなく、遅れて返った古い権限判定が保存済みのキーを上書きしないようにした。

StrictModeでeffectが再実行されたり、通信障害で再試行したりしても、URLから取り除いた招待を失わないよう、未確定の招待はrefに保持する。新しい招待の到着時には置き換え、読み込み成功後に解除する。unmount時には購読を解除し、実行中処理を無効化する。

ここでのテストは同一画面内の応答競合を扱う。複数タブ間の排他制御や、すべてのブラウザー環境での動作を保証したという意味ではない。

ユーザーにとって何が良くなるか

本番反映後は、通常・ゼロサムゲームに編集用リンクで参加した人も、管理者への依頼なしにメンバーを追加できる。同じタブで失効後の新しい招待リンクを開いた場合も、ページ全体の再読み込みに頼らず復旧できる。

検証結果

追加した回帰テストは19件。

  • メンバー画面7件: 通常/ゼロサムとeditor/ownerの組み合わせ、実際のフォーム送信、viewerでの非表示、画面表示中の権限変更。
  • 招待更新12件: 同一タブのキー再発行、権限変更、owner保持、同一役割の新キー優先、古い応答の遅延成功・失敗、通信障害後の再試行、StrictMode再実行、アンカー無視、購読解除、不正キーの除去。

最初のCIでは、新規19件は成功した一方、既存テスト1件が「editorにはメンバー画面の入口も表示しない」という古い期待値で失敗した。そのテストを「追加フォームは表示し、owner専用の名前入力・保存は表示しない」に変更した。viewerの拒否検証を削除したり、テストをスキップしたりして通過させたものではない。

Quality Gate run 37792723857 で 110ファイル・831テストが成功。依存関係のクリーンインストール、本番依存監査、期限付き例外を使う全依存監査、Lint、型検査を含む本番ビルド、Firestore Rulesジョブも成功した。

検証対象はhead d572f3d... とPR #36マージ後のmaster 7940eea... を合わせたGitHubの統合コミット e99310c833ec388dffdf5619bd0e74307f396d3f。後続のPR #35を含む最終masterとの検証とは区別する。今回はGitHub Actionsで検証し、ローカル全体テストや実ブラウザーを使う通し確認は実施していない。

ハマったこと・学んだこと

認可の回帰テストは「何も操作できない」だけでは不足する。権限ごとに許可すべき操作と拒否すべき操作を両方確認し、入口・フォーム・実行処理・APIの対応を揃える必要がある。

URLから秘密を早く取り除くことと、通信障害から再試行できることは両立させる必要がある。処理の世代管理では、表示状態だけでなく認証情報の保存まで保護対象にする。

CIが成功したこと、コードをマージできること、既存利用者を含めて本番へ切り替えられることは別の確認事項である。データ移行を必要とする変更では、テスト成功だけを理由に自動デプロイへ進めない。

保存・読取・画面復旧をつなげて検証したラウンド履歴の修正

bugfix未リリース

保存・読取・画面復旧をつなげて検証したラウンド履歴の修正

リリース状況

未リリース。 PR #30のレビュー指摘3件を検証し、修正コミット f3c7f52a68bce3df869022dd879404803bc88efe を同じPRへ追加した。マージ・本番デプロイは行っていない。以下の改善は実装・テスト段階のもので、本番反映済みという意味ではない。

何を変えたか

ラウンド番号の保存時と読取時の検証条件を共通化し、通信復旧後も残っていた履歴エラーを解除できるようにした。ラウンドの追加・変更・削除と、別端末が履歴を再取得するきっかけになる親ゲームの更新を、一括で確定するように変更した。

なぜ変えたか

読み取り時に不正な番号を拒否しても、保存側が同じ条件を保証していなければ、アプリ自身が後で読み取れないデータを作ってしまう。また、一時的な通信障害を検知するだけでは不十分で、次の取得が成功した際に画面のエラー状態も復旧させる必要があった。

履歴監視を親ゲームの変更通知に依存する構成では、ラウンドだけ保存されて親ゲームの更新が失敗すると、別端末へ変更が伝わらない。通知用フィールドは単なる補助情報ではなく、履歴の整合性を保つための更新対象だった。

本番反映後に期待する改善

不正な入力によってゲーム全体の履歴を読めなくする経路を防ぐ。通信障害後は「再試行」またはオンライン復帰で再取得できる。関連する書込みの片方だけが失敗し、保存済みの履歴と別端末の表示が食い違う状態を防ぐ。

技術的な内容

保存条件と読取条件を共有する

isValidRoundNumber を共通関数にし、正の安全な整数だけを認める。安全な整数とは、ここではJavaScriptの数値型で正確に表せる整数を指す。通常・ゼロサムの保存APIは不正な番号を400で拒否し、データベースへ書き込まない。

テストでは「POSTが成功した」だけで終わらず、その返却データを同じゲームのGETで読み出せることまで確認した。不正入力を送った後も、元の履歴は引き続き読めることを固定した。

復旧可能なエラーを分離する

履歴取得専用の roundsLoadError を追加し、ゲーム名変更などの操作エラーとは別に管理した。履歴の取得成功時に全エラーを消すのではなく、その取得に対応するエラーだけを消す。

再試行時にはAPIを呼び直し、終了した監視接続も再登録する。次のデータ更新が来ない場合でも復旧のきっかけを持てる。取得中は再試行ボタンを無効にし、古いリクエストや解除済みの監視処理が新しい状態を上書きしないようにした。

変更と通知を一括で確定する

通常POST、通常・麻雀PUT、DELETEをFirestoreのwrite batchへ変更した。write batchは複数の書込みをまとめ、すべて成功するか、どれも適用しない形で処理する仕組み。ラウンドの書込みと親ゲームの updatedAt / roundsSyncNonce の更新を同じバッチに含めた。麻雀POSTはもともとの同一transactionによる更新を維持した。

これは外部通知サービスへの配信保証ではなく、同じFirestore内の履歴と通知用ドキュメントを一緒に確定する対策である。読み取り後のあらゆる競合までwrite batchだけで解決したとは扱わない。

判断したこと・トレードオフ

不正データを0点や現在時刻へ補正する方式には戻さなかった。異常を隠すのではなく、保存時に防ぎ、取得エラーには明示的な復旧経路を設けた。

一括書込みでは親ゲームの更新に失敗するとラウンドも保存しない。片方だけ成功させる利便性より、利用者が見ている履歴と保存状態の整合性を優先した。複数のPromiseを並列実行するだけでは、片方の成功を取り消せないため、この目的を満たさない。

検証した内容

最終ソースのローカルNode.js 22.16.0環境で、npm run lint(コード検査)、npx tsc --noEmit(出力ファイルを作らない型検査)、全体テスト、本番向けビルドが成功した。テストは従来の495件から47件追加し、99ファイル・542件成功した。

追加検証には、通常・ゼロサムの番号検証とPOST→GET往復、通常・ゼロサム・麻雀のPOST/PUT/DELETEに対する障害注入、通知時の確定後履歴の読取、Reactコンポーネントの失敗→成功の描画、再試行・オンライン復帰・監視再登録・古い結果の無視を含む。

データベースの検証には、書込みを保留してからまとめて確定し、親ゲームの書込みで失敗させられるテストダブルを使った。これはFirestore Emulatorや実サービスでの競合・Security Rules検証を行ったという意味ではない。GitHub Actions側の実行記録は上記関連URLを参照。

ハマったこと・学んだこと

保存API・取得API・画面のそれぞれが単独で動いていても、一連の流れとしての整合性は保証されない。「保存できたデータが読める」「次の正常取得でエラー表示が消える」「通知を受けた時点では変更が確定済み」という境界をまたぐテストが必要だった。

単にFirestoreメソッドが呼ばれた回数を確認するモックでは、部分成功を見落としやすい。複数の書込みの途中で意図的に失敗させ、最終状態がまったく変わらないことを検証する方が、今回の不具合を直接表現できた。

継続事項

本PR全体には別途残っている認可・依存関係の課題があり、この3件の修正やテスト成功をもってプロダクト全体のセキュリティ対策完了とは扱わない。元PRに記載したインデックス準備とアプリ・Security Rulesの適用順序も引き続き必要。

有限スコアの保存契約と、改名に競合する履歴更新の修正

bugfix未リリース

有限スコアの保存契約と、改名に競合する履歴更新の修正

リリース状況

未リリース。 PR #30への再レビュー指摘2件を妥当と判断し、追加コミット 3b6b7c373d9b77f831d2e1643bb27b9c94069a33 を同じPRへプッシュした。マージ・本番デプロイは行っていない。以下は実装とテストで確認した変更であり、本番反映済みの説明ではない。

何を変えたか

スコアの保存・読み取りで有限な数値を要求する共通の変換処理を使い、無限大や数値変換のオーバーフローを保存前に拒否した。麻雀の点数移動は、1行ごとの金額だけでなく、プレイヤー別の加減算の途中結果も安全な整数であることを検証した。

ラウンドの追加・変更では、現在の参加者を読み、その参加者に対して入力を検証し、履歴と同期通知を書き込むまでを、同じFirestoreトランザクションにまとめた。ラウンド削除の存在・終了状態の判定もトランザクション内へ移した。

なぜ変えたか

読み取りを厳密にしても、保存側が同じ条件を満たすとは限らない。今回の入力正規化はNaNだけを拒否していたため、無限大を作る入力を通し、後で読み取れない履歴を自分で保存していた。データベースが無限大を拒否するという前提も置けなかった。

前回のバッチ書込みは、ラウンドと同期通知の片方だけが保存される問題を解消していた。しかし、バッチの前に読み込んだ参加者一覧が、その後も最新であることまでは保証しない。改名処理が完了した後に古い名前の履歴を保存すると、移行済みの名前が再び旧名へ戻っていた。

本番反映後に期待する改善

不正な点数を保存してゲームの履歴を読めなくする経路を防ぐ。複数端末からの改名とスコア保存が重なった場合にも、参加者一覧と履歴の名前が食い違わないようにする。旧名を含む要求は最新の参加者で再検証して拒否し、新名での再送を受け付ける。

技術的な内容

1. NaNの拒否と有限値の保証は異なる

parseFiniteScore は完全な数値文字列または数値を受け取り、変換後に Number.isFinite で有限値を確認する。書込み側と読取り側が同じ処理を共有するため、正規化だけ通り、読み取りで拒否される差を減らせる。

通常フォームで使っていた空文字・単独ハイフンを0点として扱う規則は維持した。一方、null・真偽値・不正な形の入力を、暗黙変換で0点扱いすることはしない。通常ゲームの有限な小数・指数表記は維持しており、全ゲームを整数専用に変えたわけではない。

麻雀は点数を正確な整数として扱うため、移動額と各移動適用後の残高に Number.isSafeInteger を使った。ここでいう安全な整数は、JavaScriptの数値型で正確に扱える範囲の整数を指す。最後に逆方向の移動で相殺しても、途中で失った精度は回復しないので、最終合計だけではなく途中結果を確認する。

2. 一括確定と、読み取った状態の保護を区別する

複数の書込みを一括で確定するバッチは、読取時の状態に依存しない更新には使える。ただし、参加者・終了状態などを先に検証してから保存する場合は、その前提となるドキュメントもトランザクションの読取対象に入れる必要がある。

今回の追加・変更では、ゲーム、必要な既存ラウンド、参加者検証、保存、同期通知を同じトランザクションのコールバック内に置いた。競合でコールバックが再実行されたら、参加者検証も最新データでやり直す。

入力値だけで決まる正規化は追加処理のトランザクション外で1回行える。現在の参加者にその名前が存在するか、ゲームが終了していないかという判断は、同じ場所へ出してはいけない。表示名から新しい名前を推測して変換する対応も採用しなかった。

3. 並行操作は両方の順序で検証する

テストでは実際の改名APIハンドラーと、ラウンド追加・変更APIハンドラーを組み合わせた。

  • 保存が旧参加者を読んだ後に改名を確定させると、保存側が再試行し、旧名の要求を拒否する。改名結果と既存履歴は保持する。
  • 改名が履歴を読んだ後にスコア保存を確定させると、改名側が再試行し、新しく保存された履歴も新名へ移行する。

この2つを通常・ゼロサム・麻雀の追加・変更で確認した。履歴0件、保存中の麻雀終了、変更中の削除、麻雀の同時追加や番号の上限も追加で検証した。

判断したこと・トレードオフ

トランザクションの再実行で入力の前提が変わった場合、古い名前のまま成功させず400で拒否する。書込みの成功率だけでなく、参加者と履歴の整合性を優先した。ユーザーが再送する際は最新の名前が必要になる。

読み取るゲームが共通になるため、同一ゲームへの更新は競合し得る。今回の変更はその競合時の正しさを扱うもので、実サービスの高負荷性能や再試行回数を実測したという意味ではない。

既存の破損データを0点や現在時刻で補正する方式には戻していない。過去データの自動修復や、表示名から恒久IDへの大規模なモデル変更も含めていない。

ハマったこと・学んだこと

テストダブルが「DB内の何かが変わったら必ず再試行する」実装だと、必要なゲームドキュメントを読んでいない本体コードまで、偶然救済してしまう。そこでドキュメントごとのバージョンと、読み込んだクエリ結果の変化を追跡し、実際に読んだ対象に応じて再試行する仕組みに変更した。書込み開始後の読取りも検出する。

これはFirestoreのロックや再試行処理を完全再現するものではないが、「読取対象に入れ忘れても通る」というテスト固有の見逃しを減らすために重要だった。

前回の修正で守れたのは、ラウンドと通知の一括確定だった。今回はさらに、その書込みの判断材料である参加者の鮮度を守る必要があった。「原子的に書く」と「検証した前提を守って書く」は別の観点としてレビューする必要がある。

検証した内容

ローカルNode.js 22.16.0で、コード検査、出力なしの型検査、全体テスト、本番向けビルドが成功した。100ファイル・664テスト成功で、前回から122件追加した。

不正入力は400・書込み0件・通知0件となり、既存履歴は不変更でGETも200のままになることを確認した。正常な小数や安全な整数の境界値を保存した後に、そのデータを読み出す往復テストも含む。前回の部分成功防止・再試行・画面復旧テストは維持した。

GitHub Actionsの実行記録は関連URLを参照。Firestore Emulator、実サービス、実端末での競合・Security Rules検証は未実施であり、テストダブルによる確認とは区別する。

継続事項

別途残る所有者・役割の認可と依存関係の監査は今回の2件では解決していない。元PRのインデックス先行準備とアプリ・Security Rulesの適用順序も引き続き必要。テスト成功をプロダクト全体のセキュリティ対策完了とは扱わない。

Stitch Atelier

Apple Pencilで描いた線を、編集できる刺しゅうオブジェクトとして保持するiPad専用アプリ。ランニング・ビーン・手動点列・サテンに加え、画像方式切替と穴付き面5方式、編集文書保存・PES/DST/JEF/EXP出力を実装している。main反映とSimulator確認済みの段階で、未リリース。

交差した曲線のTatamiと、原本削除後の公開管理を修正

bugfix未リリース

交差した曲線のTatamiと、原本削除後の公開管理を修正

リリース状況

未リリース。実装・独立レビュー・ローカル検証済み。配布版の反映・実機Pencil・試縫いは未確認。

何を変えたか

自己交差で囲まれた曲線の面へ、後からTatamiを追加できるようにした。編集できる曲線と開いた尾は維持し、生成用の領域だけを抽出する。ホームの「公開中」はこの端末の原本一覧ではなく、本人の公開投稿を使う。原本を削除しても投稿の詳細から公開を停止できる。画面更新で取り消された読込はエラー表示から除外する。

なぜ変えたか

内側の既定方式がTatamiになった一方、Tatamiは元の輪郭全体が閉じた単純な面であることを要求していた。以前の内側生成で使っていた閉領域抽出へ接続していなかった。また、公開一覧をローカル原本で絞っていたため、削除後に公開管理の入口が消えていた。

ユーザーにとって何が良くなったか

閉じたループを含む線を描き直す必要が減る。公開した紹介を残して原本だけを削除しても、公開・限定公開の管理へ到達できる。読み込みを取り消しただけで失敗表示が残る状況を防ぐ。

技術的な内容

Tatamiを既存の閉領域抽出へ接続し、領域ごとに穴を割り当てる。Bezierを含む正本、図案ID、Undoと文書保存は維持する。曲線の走査行の接続では領域内の安全な経路を証明し、証明できない接続は縫わない移動に分ける。

曲線で領域の境界が変化すると、行の両端だけを比較する従来判定は多数の小区画を作り、区画間の可視経路探索が処理量上限へ達した。交差面に限り、中点の安全判定で連続区画を作り、同一要求・完全に同じ輪郭の可視辺判定を再利用する。キャッシュの項目数を制限し、参照・比較・コピーも共有予算へ計上する。処理量上限自体は増やさない。従来の単純な閉領域とLegacyの経路・予算計算は保持する。

公開一覧は認証ユーザーの投稿スナップショットを使い、本人の公開・限定公開だけを表示する。公開停止の成功応答で手元の状態も更新する。CancellationError、URL読込取消、古いセッションの読込結果を表示対象から除く。実通信失敗や、書込の完了が確認できないエラーは隠さない。

Before / After

Before

見た目ではループが閉じていても、開いた線全体や交差した線としてTatamiが拒否される。ローカル原本を削除すると、残った公開投稿をホームから管理できない。

After

抽出できる閉領域へTatamiを生成する。原本の有無に依存せず、本人の公開投稿の詳細から公開を停止できる。

判断したこと・トレードオフ

  • 元の線を強制的に閉じず、正本と派生生成を分けた。
  • 安全な移動を証明できない場所はジャンプを維持する。
  • 他の面方式の交差対応へは広げていない。
  • 原本削除で公開も自動停止する仕様にはしていない。明示した管理操作を用意した。

ハマったこと・学んだこと

曲線の再現例では85行が79区画に細分化され、通常の形状処理より経路候補探索が高コストだった。キャッシュの対象を広げると既存の共有予算テストの境界まで変わるため、今回必要な派生領域に利用を限定した。公開管理では編集用原本と紹介投稿の寿命を分離する必要があった。

画像から刺しゅうを作る前に仕上がりを比べられるようにした

feature未リリース

画像から刺しゅうを作る前に仕上がりを比べられるようにした

リリース状況

未リリース。iPad Simulator で変換の導線を確認し、独立レビューを経て main に統合した開発記録。

何を変えたか

「挿入 → 画像から刺しゅう」の方式を自動とクロスステッチに絞った。仕上がり幅・糸色数・背景除去を調整し、元画像と針データの見え方を比べてから図案を作品に追加できる。Photo・Paint・Sketch の保存済み図案は既存の編集から開ける。

なぜ変えたか

輪郭のはっきりした絵を刺しゅうにしたい人が、仕上がりを見る前に解析解像度や縫い間隔まで判断する必要があった。針が移動する経路の重ね表示も、絵の再現性を見分けにくくしていた。

技術的な内容

候補の編集セッションと針順を生成して一緒に保持する。図案を追加する前に、設定・文書の世代・選択・追加先の一致を確認する。比較画面では候補の縫う線だけを描き、確定後の作品では通常どおり針の移動も確認できる。追加は一回のUndoで戻る。

単色かつ背景除去ありでは、背景候補を別の色として抽出する。画像端の大部分を占める色と一致した場合は内側も含めてその色を除き、白抜きとして残す。透明や色が混在する端では背景の推定を控える。

ハマったこと・学んだこと

白抜きを持つ細かな輪郭は、新しい面縫い向けの作品全体の共通処理上限を一律に使うと失敗した。既存作品が使う生成経路を保ちながら、追加前に完成した針順を検証する構成にした。針の見え方と実際に縫える細部は別に評価する。

不要な8項目の制作機能を削除

feature未リリース

不要な8項目の制作機能を削除

リリース状況

未リリース。ローカルのiPad Simulatorで検証した変更です。

何を変えたか

曲線区間の数値編集、尖り制限の数値指定、線の飾り、幅のある線の詳細、独立した線・曲線、生成した針への変換、接続線追加、図案全体の幅・高さ入力を削除しました。接続線と針変換のアプリ側処理も撤去しました。

なぜ変えたか

利用者が用途を理解しづらい項目を減らすため、指定された機能を削除しました。

ユーザーにとって何が良くなったか

線は手描き・点入力で作成し、図案はキャンバスで移動・拡縮する導線に整理されました。

技術的な内容と学び

旧作品の再現に必要なCoreと文書形式を保持しています。機能の削除時には、残す回転の失敗表示やボタン識別子の適用先も確認しました。複合テストは削除機能の実行部分だけを除去し、領域分割や糸割当などの保存・Undo・出力の検証を維持しました。

動作確認

iPadOS 27.0のSimulatorでアプリテスト888件が失敗0件、スキップ1件。削除項目の不在、破線・角端、保存再開、PES出力、不正入力保持もUIテストで確認しました。実機Pencilと試縫いは未確認です。

数値による配置の重複を削り、設定の開閉を1段へ整理

ux未リリース

数値による配置の重複を削り、設定の開閉を1段へ整理

リリース状況

未リリース。ローカルSimulatorで検証し、独立レビューを完了してmainへ統合済み。配布・実機Pencil・試縫いの確認とは区別する。

何を変えたか

図案の微移動・回転・反転ボタン、任意角度、作品全体の枠合わせを撤去した。図形・文字・下絵・取込素材・点入力等で、指で同じ操作ができる位置・寸法の数値欄を減らした。図案の二本指回転、下絵の指移動、輪郭点のタップ選択を整え、削除後も直接操作で編集できるようにした。

設定は目的別の最上位だけを開閉し、内側は静的な見出しへ変更した。縫い始め・縫い終わりの留め縫いと糸切りをまとめ、トグル1個だけのアコーディオンを作らない。全88箇所の開閉宣言と項目の所属、数値を残す理由を台帳に記録した。初期糸色の黒・青・赤・緑・黄・ピンクは変更していない。

尖った角・丸い角・角を落とす見本を、同じ太いL字で比較する大きな表示に変更した。描き方の選択と重複していた開始・終了・破棄ボタンも整理した。

なぜ変えたか

縫製経験のある利用者でも意味を判断できない項目や、同じ操作の数値版が重なっていた。難易度モードを増やす方法ではなく、目的と操作の重複そのものを減らすことにした。

技術的な内容と学び

  • 数値欄の削除だけでは、輪郭点の選択が最初の点に固定される。点のヒット領域だけでタップを受け、空白は既存キャンバスへ通す選択面を追加した。
  • 点の数値選択を削除した後もVoiceOverで点を選べるよう、各点を遅延作成するアクセシビリティ要素とした。activateを通常の選択へ接続し、移動時のframe追従と削除済み要素の拒否を検証した。実際の読み上げ操作は未確認として区別する。
  • 移動・回転は既存のプレビューと文書採用経路に接続し、指を離すまで正本を変えず、完了を単一Undoにする。取消・世代・対象変更では古い結果を採用しない。
  • 色相環を常時表示すると、minimumDistance=0のDragGestureが縦スクロールを奪い、意図せず色が変わる。長押しとDragの複合でも競合したため、タップ選色へ絞った。
  • イラスト自体を大きくしても、共通タイルが30ptのプレビュー高に固定していると見本が欠ける。共通部品へプレビュー高を渡し、角だけ64ptを確保した。
  • 過去作品の曲線・装飾等を開くための正本・生成処理は保持し、操作削除だけで既存作品を書き換えない。

動作確認

Appテスト890件(skip 1・失敗0)を確認。Simulatorで回転・Undo、輪郭点の選択と移動・Undo、下絵ピンチ・Undo、設定の階層・トグルの所属、縦横の角見本、スクロール時の色保持を確認した。別の回帰試験で縫い設定の適用、保存再開、PESファイル保存、不正な途中入力の保持、タップ選色を確認した。設定の全組合せ、VoiceOver実操作、実機Pencil、試縫い、最低OS、本番提供は未確認。

制作画面で図案に応じた道具を表示

feature未リリース

制作画面で図案に応じた道具を表示

リリース状況

未リリース。iPad Simulatorで確認した開発段階の記録です。

何を変えたか

配置の入力を幅・高さと回転角度に絞り、移動はドラッグと方向ボタンにまとめました。操作モードによる切り替えを廃止し、選んだ図案から点や針の編集を開けます。画像の一部分を縫う操作は画像変換に移し、仕上がりを見てから追加できるようにしました。

なぜ変えたか

同じ目的に移動差分・倍率・座標など複数の入力欄があり、項目名を理解してから操作を選ぶ必要がありました。下絵の編集には刺しゅうへの変換も混在していました。

ユーザーにとって何が良くなったか

大きさは実寸で指定できます。点を直す場合は図案を選び、画像の一部を縫う場合は画像をタップして候補を確かめられます。

技術的な内容と学び

SwiftUIの入力草稿の所有者を保ち、選択対象に応じて本文を表示します。所有者ごと外すと、対象外の図案を経由して戻った際に未適用の入力が失われます。部分画像変換では既存の候補生成と適用処理を使い、作品への反映を確定操作まで待ちます。

確認した内容

iPadOS 27.0のSimulatorで実寸・縦横比・Undo・保存再開、画像の部分変換、選択を切り替えたときの草稿保持を確認しました。実機Pencilと試縫いは別途確認します。

操作の役割を分けて右パネルの混雑を整理する

ux未リリース

操作の役割を分けて右パネルの混雑を整理する

リリース状況

未リリース。UI監査17件の実装とローカル検証を進めた段階。App Store配布、Pencil実機、試縫いの完了を示す記録ではない。

何を変えたか

4タブと同じフォームを維持し、素材選択後は一覧を短い現在素材へ畳む。編集は方式・糸色・主要寸法へ直接到達し、装飾や反復は詳細へ置いた。見本選択は84pt以上、一回の実行は44pt以上として、実行に「未選択」を付けない。

枠プリセットも数値も草稿から明示適用へ揃えた。閉じた項目に要約を表示し、正本の適用済み値と編集中の値を区別した。品質指摘は同じ理由と対処の種類で集約し、個別の位置・命令番号は選んだ指摘へ表示する。

操作モードと入力方法は端末の好みとして復元。未命名図案は順序に依存しない表示名へ変更し、既存の保存名を保つ。ガイド・点・針座標の入力を共通化し、書体は既存レンダラーで入力本文の見本を描く。

なぜ変えたか

同じ大きさのタイルに選択・設定・実行・説明が積み重なり、重要な入力が下へ押し出されていた。見た目を揃えるだけでは、何がすぐ反映され、何が適用待ちか判断できなかった。

ユーザーにとって何が良くなったか

素材を選んだ後に作業へ集中でき、基本の糸色や寸法へ到達しやすくなる。設定を開かなくても現在値が読め、並べ替え後も同じ図案を追える。再開時に使っていた編集モードの入口が消えにくくなる。

技術的な内容

SwiftUIの共通部品で選択状態を持つ操作と一回実行する操作を分離した。数値の表示文字列と未編集のDoubleを分け、表示の丸めを正本へ逆流させない。座標草稿は図案と点の組ごとに保持し、元点が変わったら古い入力を無言で適用せず、再読込を案内する。

Before / After

Beforeでは枠プリセットだけ即反映、数値は適用待ちだった。Afterでは両方が同じ草稿と適用へ接続する。Beforeでは同じ品質警告を大きいカードで繰り返した。Afterでは件数と優先度から該当位置へ進む。

判断したこと・トレードオフ

別画面や新しいタブを増やさず、既存のキャンバス・草稿・取消・Undoを保つ方法を選んだ。見本選択を小さい文字メニューへ置き換えず、実行操作だけをコンパクトにした。

ハマったこと・学んだこと

「適用すべき差分があるか」と「途中入力を保持すべきか」は別の判定になる。非activeのサテン幅に不正文字があっても、通常線の適用を止めるべきではない。一方、図案移動やUndoを理由にその入力を消してはいけない。独立レビューでこの退行を検出し、実移動とUndoの両方で入力保持を検証する回帰テストを追加した。

表示2桁の枠寸法をそのまま再解釈すると、未編集の軸まで丸められる。未編集値は元のDoubleを使い、入力された軸だけを解釈する必要がある。

編集できる線スタイルを実際の刺しゅう針へ接続する

feature未リリース

編集できる線スタイルを実際の刺しゅう針へ接続する

リリース状況

実装・ローカル検証済み、アプリは未リリース。App Storeへの配布や試縫い完了を示す記録ではない。

変更と利用者の目的

輪郭の設定で破線と位相、角と端、始点・途中・終点の飾り、6種類の重ね順・縫い順を指定できるようにした。幅はmm/cm/inch/pt/px表示に対応し、単位を変えても実寸は変えない。編集→縫い方→輪郭の縫い方→線の表現から使う。新規描画にも同じ部品を使う。

実装の要点

編集正本の点列・Bezier制御点と、出力のために分解する破線・飾り・サテン断面を分離した。破線は弧長で分割し、隙間をジャンプで移動、各区間に留め縫いを付ける。閉線の継目は連続区間を結合する。角と端は通常の固定幅サテンの実針形状として生成する。

飾りは独自の三角・丸・四角の輪郭を細い線で縫う。Bezierの平坦化サンプルでなく元のノードと接線を使い、閉線の始点と終点の方向も区別する。配布素材やGPLコードを取り込んでいない。

設定は保存形式46へ追加し、旧形式に新しいキーが紛れた場合はnullでも拒否する。Undo・コピー・候補の失効・保存再開を既存の編集経路へ接続した。未適用候補の見た目は候補の針と属性を参照し、保存と集計は確定作品のままにする。

修正前後と知見

以前は連続線だけで、破線の隙間や独立色の飾りを正本に保存できなかった。追加後は針生成・保存・出力まで同じ意味を保つ。単なる線描画APIの指定では、実際のミシン命令は変わらない点が設計の中心だった。

独立レビューでは、角落とし後も内側のマイター交点が短い辺を越え、元の線から約80mm逸脱する例を検出した。外側のマイター制限だけでは十分でない。辺ごとに隣接する内側接合が消費する長さを検査し、反転・潰れを理由付きで拒否して正本と草稿を保持した。閉じた2区間Bezierの途中飾り色が最近色へ入らない指摘も、生成と履歴で元ノードの正規化を共有して修正した。

色ホイールを常時表示すると縦スクロールを色変更として受け取り、下の設定へ進めなくなった。既存の自由色と同じ開閉へ収めた。単位切替の往復変換も最小幅を浮動小数の誤差で変更したため、表示だけの変更ではmm正本へ再代入しない。

一覧の使用糸も元の輪郭色だけでは不足する。飾りを含む生成計画の糸索引を再利用し、未生成時は確認待ちを示す。

トレードオフと残る確認

筆圧・羽毛・Bean・特殊サテン・輪郭ずらしとの角/端・破線の併用は理由付きで拒否し、別方式へ黙って変換しない。丸端・四角端は幅の半分だけ延長するため狭い破線間隔では重なる。針数と糸切替も増え得る。

Simulatorと独立readerの一致は実縫製の品質保証ではない。実機Pencil、最低OS実機、試縫い、最大文字・VoiceOverの全導線は別途確認する。

記事の切り口

  • ベクターの見た目とミシンの命令を一致させる派生モデル
  • 保存形式のnullキーまで検査する互換性境界
  • 単位切替で物理寸法を変えない数値入力
  • 色ホイールとスクロールのジェスチャー競合

確認した事実

Core全1,288件、App全865件(既定の実アカウント確認1件skip)で失敗0。iOS署名なしReleaseビルド成功。ローカルiPad Simulatorで作成・線設定・候補・適用・Undo/Redo・保存再開・PES保存・不正入力保持・縦画面・cm表示単位の再開を確認した。

保存正本を再読込してPES/DST/JEF/EXPを生成し、独立したpyembroideryで全3,460針・17色ブロックの座標と命令順を照合した。アプリからFilesへ保存したPESも、保存正本の生成計画と293針・2色・全座標RGB・命令順が一致した。独立readerの3連続jumpからの推定trimは、trim命令のないfixtureでのみ無効化し、実jumpは全て比較した。

右側4タブのリンク風操作を共通のタイルへ揃える

ux未リリース

右側4タブのリンク風操作を共通のタイルへ揃える

リリース状況

未リリース。SwiftUI実装とローカルSimulatorで検証する変更。アプリ配布、実機Pencil、試縫い、最低OS実機の確認は含まない。マージを配布完了と扱わない。

何を変えたか

挿入・編集・確認・設定で使う通常操作の共通ButtonStyleから、青文字と自動付加の右矢印を取り除く。既存の素材選択と同じ中立背景・角丸・最低高さ84ptを使い、日本語名を中央に配置する。イラストは任意とし、文字だけでも操作領域を識別できるようにする。

「手動針落ち」は素材選択の同じタイル群へ組み込み、選択中の見た目と読み上げを実際の表示フォームへ一致させる。破壊操作の赤い表示、無効状態、押下時の反応と、既存の操作・草稿の接続を維持する。

なぜ変えたか

利用者の写真では、素材が大きなイラスト付きタイルとして並ぶ一方、「手動針落ち」だけ青文字と右矢印のリンク風の行だった。同じ目的の入口が異なる見た目で混在していた。個別に飾りを追加するより、全タブへ伝播する共通スタイルを揃える方針を選んだ。

ユーザーにとって何が良くなったか

イラストの有無にかかわらず、操作できる領域を背景と余白で見分けられる。素材の入口と設定内の実行ボタンが同じ規則になり、右矢印をページ遷移のように解釈する必要がなくなる。

技術的な内容

  • EditorActionButtonStyleを変更し、4タブとUIHostingController境界のフォームで継承される表現を統一。
  • 角丸と最低高さをAtelierDesignの共通トークンへ集約し、既存選択部品と共有。
  • 手動針落ちの表示選択はUI用の判別可能な列挙型で扱い、文書素材の型や保存形式には追加しない。
  • 長い日本語は折返し、通常文字と最大文字で自然な高さを確保。回転でフォームの入力や操作の所有者を作り直さない。
  • ツールバー、数値増減、色見本、アコーディオン、主操作の専用表現は維持。移動方向を示す矢印は機能そのものの図示として残す。

ハマったこと・学んだこと

個々のButtonに矢印がなくても、継承するButtonStyleが自動追加している場合がある。見た目の差を直す際は、入口だけでなく共通スタイルとフォーム境界まで追うことで、4タブへの適用漏れを防げる。

共通スタイルの変更は縦画面での列の高さにも影響する。部品の自然サイズを測る検証と、実際にスクロールして到達・実行できるSimulator確認を分けて行う必要がある。選択式タイルと実行式ボタンの役割も、背景を揃えることとは別に扱う。

確認範囲と制約

変更前のローカル検証では、通常文字のボタンが実測44ptとなり、新しい84ptの条件を満たさないことを再現した。実装後のAppテストと4タブの操作、横縦、最大文字、無効・破壊操作の確認結果および独立レビューは、実装リポジトリの操作タイルQAと対象PRへ記録する。

100%でも手ぶれ補正が効かない入力を再現して修正

bugfix未リリース

100%でも手ぶれ補正が効かない入力を再現して修正

リリース状況

未リリース。ローカルで修正・検証。App Store配布と物理Apple Pencilの描き心地・試縫いは未確認。これは同日の初回実装に対する追加修正であり、過去の履歴は変更しない。

何を変えたか

角を保つ例外を、前後とも1/12秒以上離れた疎な入力へ限定した。補正の時定数を強度に比例する最大0.16秒へ変更し、生の入力速度によって補正を弱める処理を撤去した。採取間隔から指数応答を計算する。

なぜ変えたか

「100%にしても0%と変わらない」という報告を受け、同じ入力を0/50/100%へ与えるテストで再現した。120Hzの±0.6mm交互入力では、隣接点間が0.75mmを超えて毎回角と判断され、全強度で補正が解除されていた。速い入力では時定数も小さくなり、強度設定の効果をさらに弱めていた。

ユーザーにとって何が良くなったか

新たに描く線の揺れを強度に応じて抑える。100%では追従が遅くなる代わりに強く補正する。端点、曲線の再編集、設定保存、OFFの既存PencilKit経路は維持する。描画済みの線を設定変更だけで変形しない。

技術的な内容

alpha = -expm1(-dt / (0.16 * strength))で補正する。距離だけでノイズと意図した角を判別しない。0%はこの計算を通さない。一筆の開始時に強度を固定する既存の入力境界を維持する。

Before / After

合成入力による数値であり、実機の効果や品質を保証する割合ではない。120Hz・±0.6mm交互入力の定常区間RMSは、修正前に0/50/100%すべて0.600mm。修正後は順に0.600/0.031/0.017mm。1mm・8Hzの正弦波ノイズも60/120/240Hz・10/100mm毎秒で確認し、最大補正のRMSは0.087〜0.090mmだった。

判断したこと・トレードオフ

単純に係数を上げるだけでは角の例外で全点が素通りする問題が残るため、先にその分岐を修正した。生速度への適応をやめたため、強い補正で速く描くと先端の遅れが増える。細部も丸まるため、弱い設定やOFFも残す。

ハマったこと・学んだこと

初回テストは±0.25mmの小さなノイズで成功していたが、±0.6mmで同じ保証は成立しなかった。フィルターの基本式だけでなく、角保持などの例外条件を跨ぐ振幅・速度・採取間隔を検証する必要がある。「強設定のほうが弱設定より小さい」だけでなく、OFFに対して十分な差があることも数値で検証する。

下絵を数値とピンチで拡縮し、余分な分類ラベルを除く

ux未リリース

下絵を数値とピンチで拡縮し、余分な分類ラベルを除く

リリース状況

未リリース。ローカルiPad Simulatorで検証する変更。配布、実機Pencil、試縫い、最低OS実機は未確認。

何を変えたか

下絵の大きさを幅・高さの数値入力と大きな増減で調整し、縦横比と中心を保って適用する。明示した下絵の操作状態では、画像上の2本指ピンチで等比拡縮する。指を離すまでは派生プレビュー、終了成功時だけ既存の文書変更・Undoへ接続する。

編集の「素材の内容」「留め縫い・糸切り」はラベルも撤去し、実設定だけを並べる。挿入の描画用「縫い方」の外側開閉を除き、輪郭・内側の設定へ直接到達できるようにする。詳細の実操作と途中入力は維持する。

なぜ変えたか

下絵は幅の標準Stepperと高さ表示だけで、寸法を直接指定できなかった。既存ピンチ処理も選択図案と表示倍率の分岐だけで、下絵のサイズへつながっていなかった。前回の開閉整理では分類ラベルが残り、利用者の画像でラベル自体も不要と指摘された。

ユーザーにとって何が良くなったか

下絵を正確な寸法でも直感的な操作でも拡縮できる。下絵操作と図案・表示の拡縮を明示的に区別でき、Undoで戻せる。設定を見つけるための余分な見出しと開閉を減らせる。

技術的な内容と知見

ピンチの開始時に下絵・文書・revision・入力世代・viewport・選択を固定する。派生canvasProjectへだけサイズを反映し、取消や回転、対象変更後の古い終了通知を文書へ適用しない。数値も文字列草稿を保持し、不正値や外部の寸法変更で入力を黙って置き換えない。

静的見出しへ変えるだけでは、不要という指摘を満たさない場合がある。利用者が不要としているのが開閉操作なのか、分類名自体なのかを画像と説明から区別する必要があった。

画像PNGが同じならデコードキャッシュの更新は省略できるが、非表示/削除状態の整合まで省略してはいけない。Undo/Redoや非同期の履歴採用も通る文書の共通境界で、下絵操作モードとプレビューを解除する。直接の表示トグルだけを確認していると、この経路を見落とす。

最大文字の横スクロールでは、テストが入力欄だけを中央へ寄せると、隣の増減ボタンが画面外に残る場合がある。行の全要素を囲む枠を一緒にスクロールし、同時に収まることを検証する。

確認範囲と制約

App全844件は843成功・実Firebase1件skip・失敗0。追加8件では数値の連動・不正値・外部変更・Undo、ピンチのプレビュー/確定/取消/古い世代とUIKitの操作分岐を検証した。Simulatorでは実写真選択、数値適用、実ピンチ、Undo、タブ保持と不要項目の撤去を確認。関連導線を含む最終の分割実行結果は実装QAとPRへ残す。Simulatorでの成功は物理Pencilの描き心地や縫製品質を保証しない。

プロフィール・限定公開のアプリ実装を最新mainへ統合

infra未リリース

プロフィール・限定公開のアプリ実装を最新mainへ統合

リリース状況

iPadアプリは未リリース。今回はアプリ実装のmain統合とローカルSimulator検証を対象とし、本番への手動デプロイやアプリ配布は行わない。前日のバックエンド反映記録とは区別する。

何を変えたか

作業ブランチに残っていたプロフィール写真の選択・保存・削除、公開作品の作者画像、限定公開リンクの発行・失効、アカウント削除前の確認を、最新の編集画面と共存する形で統合した。写真の色再現や下絵の拡縮など、既存mainに入った機能を保持する。

なぜ変えたか

コミットとワークツリーの調査で、バックエンドの反映記録とアプリmainの実装状況が一致していないことが分かったため。コミット番号が異なるだけで統合済みの機能と、実際に未統合の機能をコードで区別した。

ユーザーにとって何が良くなったか

対応開発版でプロフィール画像と限定公開の操作を使える状態へまとめる。一般配布済みの機能として告知するものではない。

技術的な内容と学んだこと

  • 未マージ表示だけでは統合漏れと判断できない。別コミットで取り込まれた修正を、パッチと現行コードの両方で照合する。
  • デザイン規約の競合は古い文書全体で置換せず、最新mainの節と配置を保持して機能固有の仕様を追加した。
  • Functions単体49件、demo専用Emulatorの権限・画像差替・リンク失効・HTTP共有ページ31件が成功した。Emulatorでは既存Java21のパスを明示する必要があった。Functions宣言のNode22とホストNode26でのローカル実行は区別する。
  • XCUITestで同じ名前の元ボタンと確認ダイアログの操作を firstMatch だけで選ぶと、元画面へ誤って操作する場合がある。元ボタンの識別子を除外して確認操作を特定し、取消・閉鎖の状態遷移を待つ。
  • 本物のテストアカウントは作成・削除・確認状態の変更をしない。変更系の正常・異常動作はdemo Emulatorで確認し、Simulatorの実接続確認は既存アカウントのログインと取消経路を使う。

写真の色と輪郭を保持するPhoto刺繍

feature未リリース

写真の色と輪郭を保持するPhoto刺繍

リリース状況

未リリース。iPadネイティブアプリのローカル実装。Core専用13件・全1266件、最新main統合後のApp全849件(848件成功・Firebase接続1件skip)を確認。Simulatorで実写真の制作・設定変更・Undo/Redo・保存再開・回転・PES出力を確認し、独立した読み取りで全座標・色・命令の一致を確認した。独立レビューで再解析時の旧色番号の持越しを修正し、修正後の全件・通常UI導線と出力照合が成功し、再レビューで必須指摘なしと判定された。PR #32へ統合記録を残す。App Store配布・試縫いは未確認。最終結果は対象PRへ記録する。

何を変えたか

写真から最大16色を抽出し、色領域の境界と透明部分で区切った短い針を作るPhoto方式を追加。新規は12色、解析256、列間隔0.4mm、最大針長2.5mm。従来の単色・CMYKの帯方式と、元画像・設定を持つ編集可能な正本を保持する。

なぜ変えたか

従来の帯中心サンプルは幅100mm・解析256画素・帯1.2mmだと84列だけを読むため、未参照列の細線が消える。さらに、実針の上に全jumpのオレンジの破線が重なり、写真の色を画面で判別できなくなっていた。

ユーザーにとって何が良くなったか

肌・髪・服などを写真に近い実際の糸色で表し、色境界をまたぐ不要な縫い線を減らす。新方式の移動経路は初期非表示にし、確認設定から明示表示できる。既存の作品を開くだけでは針を変えない。

技術的な内容

Swift 6の独立生成器で、4bit histogramの元RGB平均を保持した減色、4近傍連結領域、色ごとの往復する列、針落ち位相のずらしを実装。接続線は画素格子との全交点で区切って区間を検査し、透明画素の細い角を一様サンプルで見落とさないようにした。近接する同色の独立領域の経路を別々に保持する。

新方式と退避履歴はschema45。PNG・RGBA・設定・パレット上書き・登録糸を保持し、編集文書とミシン用の針データを区別する。画像・寸法・色数が完全一致する解析を同一リクエスト内でだけ再利用し、照合と残り処理も共有予算に含める。

Before / After

従来CMYKと新方式を、NASAの公開ポートレート1枚、256解析、100mm四方、同じ0.35mmの目安糸幅で比較した。元画像に対するRGB平均絶対誤差は75.4762から30.1004へ低下。42774針から38361針へ変化した。この1枚の描画比較であり、写真一般の再現性や実縫製の品質を保証する数値ではない。新方式は9901runで、細かな領域と糸の移動が増える制約もある。

判断したこと・トレードオフ

Ink/Stitchの領域での切抜き・針落ちずらし・経路の考え方を調査した。GPLv3以降のコードや素材は取り込まず、Swiftの独自方式を採用した。解析最大256画素・最大16色で、実寸と列間隔より小さい細部は保持できない場合がある。実際の糸・生地・針数・移動のバランスは試縫いで確認する必要がある。

ハマったこと・学んだこと

  • 解析解像度を上げても、後段で帯中心だけを読む処理では細部を拾えない。
  • 生成データが改善しても、移動経路の診断表示が写真全体を覆うと、ユーザーには改善して見えない。実針の表示と診断overlayを分ける必要がある。
  • 同色領域を一つの前行状態だけで追うと、別の島の状態が上書きされ不要なjumpが増える。
  • 作成・形状検証・針数確認・出力の各段で画像を解析し直すと、単体生成は高速でもリクエスト全体の予算を超える。入力が同じ場合だけの短命キャッシュで重複を減らした。
  • 抽出色の番号は解析結果内の一時的な識別子で、解像度変更後の同じ番号が同じ色とは限らない。解析データが変わる場合は写真色の上書き・登録糸を解除する説明を置き、同じRGBAなら保持する。色数減少・同数で順番変化・非active設定・保存・Undoを回帰確認した。
  • 大きな原点と極小軸では、数学的に非退化でも浮動小数点の丸めで四隅が一致する。count-onlyでも拒否する。

写真・輪郭・針ファイルを取り込む前にサイズを確認する

feature未リリース

写真・輪郭・針ファイルを取り込む前にサイズを確認する

リリース状況

未リリース。iPadアプリへ実装し、ローカルSimulatorで検証している段階。配布・実機操作・試縫いは未確認。

何を変えたか

写真、Filesの下絵、SVG、DST/JEF/PES/PECを選ぶと、作品へ追加する前にキャンバスへプレビューを表示する。二本指ピンチと幅・高さのmm入力で大きさを選び、右下の確定で採用する。既存下絵の置換も同じ明示確定へ変更した。オレンジ色の破線は縫わない移動経路であることも共通キャンバスに常設する。

なぜ変えたか

選択ファイルの名前だけでは、刺しゅう枠に対してどの程度の大きさになるか判断できなかった。取り込んだ後で調整する手順を、取り込み前の確認へ移す要望があった。

ユーザーにとって何が良くなったか

入口や素材の形式にかかわらず、見てサイズを決めてから取り込める。ピンチは表示倍率ではなく未確定素材のサイズを変える。取消は作品を変更せず、確定後は1回のUndoで戻せる。

技術的な内容

SwiftUIのフォームが型付き素材と数値草稿を所有し、現在のフォームだけを共通キャンバスへ公開する。画像は元PNG、SVGは輪郭、針データは命令・糸情報を保ち、プレビューのために平坦化しない。キャンバス入力はUIKitのピンチ認識器で終了と取消を区別する。確定時だけ既存の世代検証付き非同期writerへ渡す。

判断したこと・トレードオフ

専用の別画面ではなく既存の刺しゅう枠と並べて確認する。縦横比と中央配置を保ち、幅と高さは連動させる。大きなファイルは枠内へ収めた候補を表示し、確認したサイズを採用する。針データを拡縮すると針間隔も変わることを説明する。

ハマったこと・学んだこと

  • 非対称の針座標を中心配置するとき、プレビューの開始位置も確定後の針計画へ揃える必要がある。停止・色替え・糸切りに位置が付く場合の移動も、表示から落とさない。
  • 読込を取り消す操作だけでは不十分で、保留元と再試行経路を一緒に保持する必要がある。解析失敗や取消で素材を選び直させない。
  • 数値の途中入力と最後の有効倍率を分ける。不正文字を無言で有効値へ戻さず、ピンチと確定を止めて修正できるようにする。
  • UI自動テストではボタンのAX領域に説明文が含まれる場合があり、中央タップが実ボタンに当たらなかった。表示ラベルの座標と画面を照合し、製品のpicker不具合という仮説を棄却した。

検証と今後

最終App対象23件が成功し、寸法連動・ピンチ取消・輪郭/針の保持・単一Undo・既存writerの回帰を確認。Simulatorでは写真の実ピンチ・回転・確定寸法、Files画像の数値入力・取消・再選択・増減・確定、SVG/PESの実ピンチ・確定・Undoが成功した。独立レビューの先頭針経路と再試行の指摘を修正し、再確認で追加の必須問題なし。検証用に作成した一時Simulatorは証拠回収後に削除した。最低OS、最大文字、実機Pencil、試縫い、配布は別途確認する。

説明の開閉を減らし、写真から刺しゅうを作る入口を独立させる

ux未リリース

説明の開閉を減らし、写真から刺しゅうを作る入口を独立させる

リリース状況

未リリース。実装とローカルSimulatorの検証・独立レビューは実装リポジトリの写真0059〜0061 QAへ記録する。マージをアプリ配布と扱わない。実機Pencil、試縫い、最低OS実機は対象外。

何を変えたか

説明しかないアコーディオンを通常の文章へ変更する。「下絵画像」と「画像から刺しゅう」の素材入口を分け、なぞるための参考画像と変換方式の選択を探す場所を明確にする。編集・確認・設定の実設定は閉じた一覧から始める。挿入で選んだ素材の入力は初期展開を維持する。

なぜ変えたか

実機の画面への書き込みで、説明を読むだけの開閉、下絵フォームの中に隠れた変換機能、長い設定面で項目を一覧しにくい点が指摘された。

ユーザーにとって何が良くなったか

説明を読むためだけに開閉する必要がなくなる。写真から刺しゅうを作りたい場合は素材一覧から直接選べる。編集は項目を見渡して必要な操作だけ開ける。

技術的な内容

SwiftUIの素材列挙型と常設フォームを使って入口を分ける。画像変換の生成処理や編集可能な画像正本は再利用する。画像がない場合は既存の写真・Files選択と保留・キャンバス右下確定から準備し、下絵として保持する画像を変換元に使うことを説明する。

判断したこと・トレードオフ

共通の初期開閉を閉じた状態にし、選択した挿入素材だけ明示的に展開する。表示を条件ごと作り直す方法は避け、フォームの所有者を保って途中入力や開閉を維持する。変換元と下絵の保存構造は今回は分離せず、画面上の目的と入口を分ける。

ハマったこと・学んだこと

共通の開閉部品に初期値があっても、静的な分類見出しには作用しない。画像の説明枠、実操作の枠、分類だけの枠をコードから見分ける必要がある。別素材へ生成フォームを移す際は、表示条件だけでなく処理後の状態更新、非表示フォームの確定登録と、既存UIテストの経路も追う。

静的見出しを開閉へ変えると、従来の本文に掛けた disabled が新しいヘッダーにも及ぶことがある。独立レビューでは、ロック中に解除案内を開けない問題を検出した。開閉を有効にし、穴・点・数値など実際の変更操作だけを無効にする境界へ移した。ロック中の理由表示と変更抑止、解除後の入力有効化を同じSimulator導線で確認する。

確認範囲と制約

入口分離、画像なしからの準備、実画像選択・変換、編集の初期折りたたみ、途中入力・開閉のタブと回転での保持をローカルで確認し、対象コミット付きの結果をQAとPRへ残す。配布前のお知らせとして掲載しない。

描画中の手ぶれ補正と編集可能な曲線の保持

feature未リリース

描画中の手ぶれ補正と編集可能な曲線の保持

リリース状況

未リリース。手ぶれ補正の実装とローカル検証を進めている。App Store配布、物理Apple Pencilでの遅延・筆圧追従・描き心地、実ミシンの試縫いは未確認。競合と同じ算法を実装したとは表現しない。

何を変えたか

手描きの入力設定にOFF/弱/標準/強を設け、詳細で0〜100%を指定できるようにした。初期値は標準50%。線と輪郭、手描き穴に使い、点入力・手動針・マス描画は変えない。設定はiPad内の入力の好みとして保存し、描画済みの形や文書のUndoを変えない。

なぜ変えたか

従来のPencilKit曲線の保持や、描いた後に点列の角を丸める操作だけでは、描画中の揺れを利用者が調整できなかった。筆圧サテンの幅変化を滑らかにする設定とも区別する必要があった。

ユーザーにとって何が良くなったか

描きながら揺れを抑え、強度を選べる。補正後もBezierの点と制御点を残すため、確定後に形を編集できる。強くすると追従がゆっくりになるので、その点を画面でも説明する。

技術的な内容

Swiftのストリームフィルターへmm座標と採取時刻を渡し、移動速度に応じて追従を変える。ライブ表示と確定処理で同じ曲線を使う。ONでは専用のUIKit認識器とCAShapeLayerを使い、OFFは既存PencilKitの曲線経路を保持する。針データから編集用の形を逆生成しない。

一筆の開始時に座標変換・表示世代・入力世代・強度を固定する。二本指の表示操作、回転や対象変更、取消で仮入力を破棄し、既存文書は保持する。筆圧は同じ採取時刻の補正位置へ対応させてから曲線の弧長へ写す。

判断したこと・トレードオフ

  • フィルターの強度は書き出しや文書の設定とは分離した。設定変更だけで過去の図案を変えない。
  • 端点は最後の実入力位置へ追いつかせる。描画中の先端付近と離筆時の追従を、既存の形の更新と混同しない。
  • 計算量と点数を制限し、上限超過では別の粗い線へ無言で置換しない。
  • 小さな細部も補正で変わり得る。Simulator成功だけで実機の描き心地や縫製品質を保証しない。

ハマったこと・学んだこと

角を残そうとして隣り合う生入力の向きが急変したら補正を外すと、細かな往復ノイズまで角として残ってしまった。交互に揺れる直線のテストで抑制が働いていないことを検出し、疎で明瞭な頂点と小さな揺れの判定を分けた。

個々の時刻が有限でも、非常に大きな正負の時刻差は無限大になり得る。座標・時刻だけでなく差分時間も検証し、失敗時は最後の有効なプレビューを保つ必要があった。

開閉を一段減らし、写真選択と設定幅を共通の入力保持へ接続する

ux未リリース

開閉を一段減らし、写真選択と設定幅を共通の入力保持へ接続する

リリース状況

未リリース。iPad向けSwiftUI実装とローカルSimulatorでの確認対象。アプリ配布、実機Pencil、試縫い、最低対応OS実機での確認は行っていない。

何を変えたか

設定の「作品管理」「表示・配置補助」、編集の「素材の内容」「留め縫い・糸切り」「形・点」等で、子の開閉項目だけを包む外側の開閉を静的見出しへ整理する。計測・縫い順・従来の糸色も、実操作へ到達する開閉を一つにする。

操作のない専用素材の縫い方は静的表示。キャンバスで選択・移動してもタブを変えず、マスの再編集を編集タブ内で完結する。Magic Wandで選んだ領域の追加も、共通のキャンバス右下の確定へ接続する。

下絵の選択元に写真ライブラリを追加し、Filesと同じフォームから選ぶ。新規は選択を保留し、右下確定後に既存の画像正規化と文書トランザクションで取り込む。読込取消・失敗・遅延結果で既存の保留や作品を壊さない。

なぜ変えたか

各フォーム部品は単体で開閉を持っていたが、親画面にも分類用の開閉を追加したため、実設定へ到達するまで同じ目的で二度開く箇所が残った。利用者の画像では、写真から選ぶ入口の不足と、長いラベル・トグルの左右見切れも指摘された。見切れは糸切り一箇所ではなく共通レイアウトの問題として扱う。

ユーザーにとって何が良くなったか

開閉の回数を減らし、選択したタブで作業を続けられる。下絵をFilesへ移す準備なしに写真から選べる。広い横画面では右パネルを最大360ptへ広げ、利用可能な内容幅を子へ渡す。長いラベルとスイッチが横に収まらない場合は縦へ並べる。

技術的な内容

  • 分類用の共通静的見出しと、実設定のアコーディオンを分離。保持フォームのStateと入力を作り直さない。
  • UIKit hosting境界を通る環境値に、実際の内容幅と共通ToggleStyleを渡す。最大文字の折返しとネイティブスイッチの操作を保つ。
  • PhotosUIの選択は保留草稿へ格納。読込ID、取消、文書・revisionの一致で遅延結果を除外する。画像の採用成功時も一致する保留だけ解除する。
  • 新規の写真選択と、既存下絵の置換は区別し、正本・Undo・保存の既存経路へ接続する。

ハマったこと・学んだこと

コード上でアコーディオンの個数を見るだけでは、実際に選択された素材・モードで中身が子見出しだけになる条件を拾いきれない。親と子の組合せで所有者を確認し、実導線の回帰を残す必要がある。

ネイティブスイッチのアクセシビリティ要素の枠は、見えている行全体の高さと同じとは限らなかった。左右の包含を検証するテストへ無関係な高さの仮定を混ぜず、共通部品のサイズ検証と実操作確認を分ける。

確認範囲と制約

最新main統合後のCore全1,250件、App全834件(833成功・実Firebase1件skip・失敗0)をローカルで確認。保留選択・取消・失敗・古い写真結果の不採用・Undo、パネル幅と文字拡大のサイズを回帰へ含む。Simulatorでは階層・タブ保持・写真の実選択と保留/確定・回転を確認する。UI各ケースとレビューの最終結果は対応する実装QAとPRへ記録する。Simulatorの成功は実縫製品質や実機の描き心地を保証しない。

手ぶれ補正の一筆容量を約2倍へ拡張

bugfix未リリース

手ぶれ補正の一筆容量を約2倍へ拡張

リリース状況

未リリース。ローカルで実装・検証する変更であり、アプリ配布と物理Pencilの受入は別に行う。

何を変えたか

一筆の入力サンプルを16,384から32,768、編集可能なBezier曲線を1,024から2,048区間へ増やす。補正中に保持する点は、離筆位置への追従と自動閉鎖の両方を確定できるよう2,047点までにする。従来1,024点の約2倍。筆圧側の入力上限も揃え、曲線への投影予算を200万から400万へ増やす。

なぜ変えたか

手ぶれ補正の描画ですぐ「データ量が上限を超えています」となるため、約2倍にしてほしいとの依頼があった。補正点だけ増やすと、離筆時の筆圧計算や曲線の確定・編集・面境界近似が旧上限で止まるため、関連する制限を揃える必要があった。

ユーザーにとって何が良くなったか

一筆でより長く描け、補正後の曲線を編集・保存できる。新しい上限を超えた一筆は部分確定せず、描画中の最後の有効プレビューと既存草稿・作品を保持して描き直しを案内する。補正の強さと算法は変更しない。

技術的な内容

曲線区間数と入力サンプル数の上限をそれぞれ共通定数にする。曲線の平坦化・延長・分割、編集UIと面境界近似で同じ区間数を参照する。ファイル容量、作品全体の点数・針数、各縫い方の生成予算は維持する。

ハマったこと・学んだこと

入力の最大点数と出力の最大区間数は一致しない。離筆位置を追記すると1区間増え、閉じた形にするためにさらに1区間増える可能性がある。上限を単純に同じ数へ変更すると、描画中は成功するのに離筆や確定で失敗する。終端と閉鎖用の余地を含めて境界テストを行う。

挿入の確定位置を揃え、文字の画に沿ってサテンを生成する

improvement未リリース

挿入の確定位置を揃え、文字の画に沿ってサテンを生成する

リリース状況

未リリース。iPad向けローカル実装とSimulator検証の記録であり、配布や実機での試縫いの完了を意味しない。PRのマージ状況と最終検証結果はアプリ側のQAへ記録する。

何を変えたか

挿入の確定を既存の「線を確定」と同じキャンバス右下へ統一する。文字・図形・曲線・QR・画像からの生成・ファイル取込が対象。ファイル選択は保留にし、右下で確定してから編集用の正本へ追加する。作品管理と出力前確認は、設定と確認のそれぞれ同じスクロール面に展開する。

空の設定見出し、内側のアコーディオンを包むだけの見出し、常に無効になる汎用操作、処理が接続されていない「戻る」を調査して整理する。作品詳細の制作再開を先頭へ移動し、文字の書体はその場で選べるようにする。

文字サテンは固定角度で文字全体を横断する方式から、輪郭から中心線の局所方向を推定し、各部分の幅を横切る方式へ変更する。

なぜ変えたか

実機での変更メモから、確定場所の不統一、開いても使える内容がない設定、右パネルが別の画面へ切り替わる挙動が指摘された。文字サテンも、縦画と横画を同じ角度で横断しており、期待する縫い方向と異なっていた。

ユーザーにとって何が良くなったか

設定をスクロールしても確定の位置が変わらず、入力してから確定する流れが揃う。選択しただけでファイルが追加されない。設定の内容を別の画面へ差し替えず、元の位置や入力を維持する。操作モードは実際に点編集・面演算・針命令の表示を変えるため維持する。

技術的な内容

入力フォームが草稿と生成条件を保持し、有効なフォームの確定操作だけを共通キャンバスへ登録する。UIKitのhosting境界を使う保持フォームではSwiftUIのpreferenceが別ツリーを自動では越えないため、同じ登録先と有効状態を明示的に渡す。非表示フォームの登録解除と、設定欄だけを閉じる操作を区別する。

文字の方向推定には限定ラスタの細線化を使い、針の端点は元のベクトル輪郭との交点へ戻す。nonzeroの穴を維持し、接続線が穴・外側・文字間を横切る場合はジャンプにする。針長の分割と保存・Undo・書き出しは既存処理を使う。

Before / After

Before

文字の追加は設定欄、描いた線の確定はキャンバス右下。作品管理を開くと右側の設定全体が別画面へ切り替わる。文字サテンは一定角度の走査線で生成する。

After

挿入の確定をキャンバス右下へ集約し、管理と出力前確認を所属タブへ展開する。文字の各部分から方向を求め、幅を渡るサテンを生成する。

判断したこと・トレードオフ

  • 編集用の輪郭正本をラスタへ置き換えず、ラスタは方向推定だけに使う。
  • 方向推定には最大0.2mm刻みの近似があり、極細部と分岐の縫い順は試縫いと別の品質評価が必要。
  • 16万セルと共通200万作業単位の範囲で処理し、超過時は入力を保持して失敗を返す。無言で低解像度へ落とさない。
  • 処理中やロック時の一時的な無効化は必要な機能として残し、常に使えない操作と区別する。

ハマったこと・学んだこと

  • 機能があることと、ボタンが実際に呼び出す処理を持つことは別。空のコールバックが渡された「戻る」は、見た目だけでは見抜けない。
  • 外側に開閉見出しがあっても、条件付きの子が空なら利用者には空のアコーディオンになる。親の表示条件と子の利用条件を揃える。
  • 空の登録を解除するとき、未登録のnilと空配列を別扱いすると、何も変わっていないのに観測更新を繰り返す。正規化して比較する。
  • 中心線から方向を推定しても、別の画や穴へ接続しないことは別途元輪郭で検査する必要がある。

閉じた面から12種類のクロス模様を生成する

feature開発中(未リリース)

閉じた面から12種類のクロス模様を生成する

リリース状況

未リリース。独立Core・関連Appの検証後、Circular/Contourと共通UIへ統合して機能別E2Eを進めている。可視入力欄の操作性と独立readerも追加確認中。最終レビュー、親Simulator受入、CI・マージ、App Store提供は未完了。

何を変えたか

編集可能な閉パスと穴から、12種類のクロス模様を生成する面縫いを実装中。画像から作るクロスや手描きのマス正本は維持し、面から生成する方式とは区別する。

なぜ変えたか

既存のマスを塗るクロスだけでは、輪郭を後から編集して面全体へ模様を作り直す使い方を扱えない。図形の正本を残したまま、格子と模様の設定を変更できるようにする。

ユーザーにとって何が良くなるか

模様、縦横寸法、作品基準/図案基準、角度、格子のずれ、境界セルの被覆率、線の重なりを調整する入口を用意する。被覆率で採用したセルには完全な模様を置く契約であり、境界で全ての針を切り落とす面縫いと同じとは表示しない。

技術的な内容

方式設定は共通のfillMethodとschema43へ接続する。既存のセルCrossへの変換や、生成済み針への平坦化は行わない。図案基準の格子は保存した基準点・向きを使い、変形後にもmm単位の意味を保って再生成する。

セル採否の幾何には上限付きの有理数計算を導入した。頂点・候補・処理量・針数と取消を制限し、上限を超えたときに粗い模様へ無言で置き換えない。

ハマったこと・学んだこと

  • 境界の被覆率では、丸め方の選択だけで100%判定を安全にできるとは限らなかった。丸めに起因する負例を受け、独立した境界oracleと上限付きの厳密計算を組み合わせた。
  • 新しく描く図案が、直前の図案基準点を再利用する問題を実際の描画callbackを通したテストで発見した。図案ごとに基準点を解決し、二つの図案で別の基準になることを検証した。実機Pencilの成功を意味するものではない。
  • 画面に入力欄が完全に見えていても、isHittableだけがfalseになるケースをE2Eで捕捉した。重複要素、可視ancestor、実際のタップによるfocus・値更新を分け、描画位置だけで操作可能と断定しない。
  • 独立readerでも、針列が一致するだけでは想定外のjumpを排除できない。命令の位置・順序を含めた照合と、追加jumpを見逃さない負対照を強化している。

列と区画で選ぶ面縫いの実装と共有処理予算

feature開発中(未リリース)

列と区画で選ぶ面縫いの実装と共有処理予算

リリース状況

未リリース。Tatami/Legacyは機能別の単体・E2E・Release確認を行い、初回の独立レビューで見つかった4点を修正している。親のSimulator受入、最終レビュー、CI・マージ、App Storeでの提供は未完了。

何を変えたか

通常の閉じたパスの内側へ「列で埋める(Tatami)」「区画ごとに埋める(Legacy)」を追加する実装を進めている。共通の間隔、最大針長、糸、穴、下縫いを保ち、方式固有のずらし・位相・行末・区画順を別の型付き設定へ保存する。

なぜ変えたか

面縫いの名称だけを並べても、実際の経路が同じでは選ぶ意味がない。既存方式を保持しながら、列をつなぐ方法と、区画を縫い終えて次へ進む方法を別の生成処理として扱う。

ユーザーにとって何が良くなるか

同じ形・糸・穴を保って縫い方を比較し、候補を取り消して元に戻せるようになる。数値入力の途中や方式を往復した草稿を、適用やパネルの閉再開で失わないことを検証している。

技術的な内容

新設定はPathInteriorStyle.fillMethodを唯一の方式正本とし、明示的に利用したときだけ必要なschema43へ昇格する。旧nilは旧生成器へ送り、既存文書を開いただけで設定や針列を変えない。schema41/42との混在は必要versionの最大値を使う。

候補の表示用計画と確定文書を分け、保存・集計・ミシン用出力は確定正本を使う。下縫い、上糸、輪郭、留め縫い、糸切りの順序と設定を維持する。

ハマったこと・学んだこと

  • 単体で各生成器に上限があっても、呼び出す前の幾何検証や、もう一度作られるcomponent、輪郭生成の処理が共有予算外なら、要求全体の上限にはならない。初回レビューで見つかり、処理前の予算境界と生成済みcomponentの再利用を修正している。
  • 安全判定の最後の比較が予算をちょうど使い切る正常結果と、次の比較が払えない失敗は異なる。単に残りが0かを見ると前者まで拒否するため、上限ちょうどと1不足を別の負例にする。
  • 古い非同期結果を捨てるだけでは、二つの重い生成処理が同時に始まることを防げない。面縫いと既存画像・変形・clipboard処理の相互抑止を、遅延workerで確認する必要があった。
  • 針座標が全て一致しても、命令の件数とjump終点だけでは色替えや糸切りの位置を保証しない。独立readerを、形式差を正規化した命令の位置と順序まで照合する形へ強化している。
  • 同じ数値でも、UIへ「8」と入力する場合と「8.0」と入力する場合は同じ操作ではない。待機問題の最小再現でこの違いを訂正し、同一helper・同一文字列・同一端末で変更前後を比較した。

最新UIへ既存mainの機能を統合

improvement未リリース

最新UIへ既存mainの機能を統合

リリース状況

未リリース。アプリの統合PRで検証。Functionsの本番デプロイ、実機Pencil、試縫いは未実施。

何を変えたか

設定整理のセッションで承認されたUIを正本とし、既存mainの線反復・特殊サテン・中心下縫い・候補プレビューを、その4タブとアコーディオンへ組み込んだ。main側の外付け固定フッターやサイド操作欄は採用しない。

「編集 → 縫い方」の対応する子項目に設定・プレビュー・適用をまとめ、処理状態・失敗・取消を共通位置へ集約した。設定を閉じると関連操作も閉じ、未確定候補は共通位置から取り消せる。

なぜ変えたか

mainのコミットが先行していても、UIの承認が最新とは限らない。利用者が今回のセッションを最新のデザインと指定したため、機能を失わず、承認された配置へ合わせた。利用者は機能追加のたびに操作場所を学び直さずに済む。

技術的な内容

候補のキャンバス描画・針順再生と保存済み文書の統計を分離したまま、共通のアコーディオン部品へ操作を配置する。表示中の編集面と親の展開状態を方向編集の条件に含め、縦画面で図案一覧を使った後に横画面へ戻っても、見えている編集面と方向編集の有効条件を一致させた。

ハマったこと・学んだこと

  • Gitの取り込み順序とデザインの承認順序は別に確認する。競合解決では機能の存続と画面配置の採否を分けて判断する。
  • 設定の中へ操作を戻す際は、閉じた後にも必要な取消を同時に設計する。状態表示の重複も確認する。
  • XCTestでStepperを操作する場合、コンテナ全体ではなく実際の増減ボタンを対象にする。コンテナの到達判定でテストが停止しても、アプリのレイアウト不具合とは限らない。

検証と今後

統合後のアプリ・Coreテストは1,765成功、0失敗、実Firebase接続用1件skip。Functionsのドメインテストは10成功。対象Simulatorの結果と未確認事項はアプリの docs/design/qa/2026-10-30-session-ui-integration/QA.md に記録する。この記事材料をリリース告知として扱わない。

プロフィールと限定公開のFirebaseバックエンドを反映

infraリリース済み

プロフィールと限定公開のFirebaseバックエンドを反映

リリース状況

バックエンドのみ本番反映済み。iPadアプリは未リリースで、アプリの配布や全利用者への機能提供開始を意味しない。作品販売は今回も実装していない。

何を変えたか

プロフィール画像と限定公開のFunctions、Firestore/Storage Rulesを本番へ反映した。新規7Functions・既存6Functionsの計13個がNode22・asia-northeast1でACTIVEになっていることを確認した。

なぜ変えたか

アプリ側に実装したプロフィール画像の保存・作者表示と、失効可能な共有リンクを実際の接続先で利用するため。

ユーザーにとって何が良くなったか

対応する開発版アプリから新しいバックエンドへ接続できるようになった。一般配布されたアプリの更新完了としては扱わない。

技術的な内容と学んだこと

デプロイ直前に最新mainの複数カテゴリ対応を取り込み、カテゴリ配列と公開範囲を両立させた。公開時はすべてのカテゴリで検索tokenを生成し、限定公開・公開停止では検索から除外する。共有DTOも全カテゴリと旧単一カテゴリの読込互換を維持する。

デプロイに成功したことと、通常の利用がすべて成功することは分けて記録した。デプロイログに加え、13Functionsの稼働状態、本番の無効リンク404とキャッシュ禁止、未認証のプロフィール変更401を確認した。確認のためにユーザーや作品を作成・変更・削除していない。

検証

Functions単体49件成功、demo Emulator30件成功・HTTP opt-inの1件skip・失敗なし。統合後のアプリとUIテストのビルド成功。Firestore/Storage Rulesのリリースと全Functionsの作成・更新が成功した。本番の有効リンク閲覧・プロフィール画像保存の正常系は今回未確認。

プロフィール画像と失効可能な限定公開リンク

feature未リリース

プロフィール画像と失効可能な限定公開リンク

リリース状況

未リリース。GTRの作業ワークツリーで実装・検証中。本番Functions・Firestore/Storage Rulesの反映、配布済みアプリからの利用は未確認であり、提供開始の告知ではない。作品販売は将来の検討事項として記録し、価格・決済・注文・販売者登録は実装していない。

何を変えたか

写真からプロフィール画像を選び、中央を正方形にした草稿を確認して保存する。公開作品の作者名と画像を一緒に表示する。未登録の画像は人物アイコンとなる。表示名は一つで、作品の作者名は公開時点の名前を使う。作品名と作者名の前方一致検索を維持する。

アカウント削除のトグルを赤文字のボタンへ変更し、説明モーダル・本人確認・最終確認を経る導線にした。編集用原本はこのiPadに残す。

作品の紹介は公開・限定公開・公開停止を分ける。限定公開は一覧・作者名検索から除外し、専用リンクを知る人だけがiPad/共有Webページから閲覧できる。閲覧はログイン不要、いいね・お気に入り等には会員条件が必要。作者は期限なしのリンクを無効化・再発行できる。

なぜ変えたか

作者の識別を分かりやすくし、プロフィールの名前の重複表示と誤って退会しやすい操作を整理するため。作品は広く公開する場合だけでなく、相手へリンクを渡して紹介したい場合にも対応する。

ユーザーにとって何が良くなったか

作品の作者を名前と画像から判断できる。削除は確認と本人認証を経て確定する。限定公開では受取人が登録前に作品を見られ、保存や反応を行う段階で会員登録へ進める。

技術的な内容

SwiftUI・PhotosUIで写真を受け取り、ImageIOで縮小してから再描画する。公開JPEGは512px以下・1MiB以内、読込元は20MiB以内に制限し、元写真のGPS・コメント・カメラ情報を引き継がない。公開プロフィールの文書は画像パスと更新日時だけとし、メール等の本人専用情報を分離した。

共有リンクは32バイトの暗号学的乱数から作り、データベースにはハッシュだけを保存する。限定公開のデータと画像はリンクを検証する経路を通し、一般一覧やStorageの公開参照から取得させない。所有者の管理経路は別に許可する。共有WebはFirebaseのHTTP関数で画像・作者・紹介を表示し、HTMLエスケープ、noindex、no-storeを設定する。

判断したこと・トレードオフ

  • 作品のタイトルや紹介の更新だけではリンクを変更しない。明示的な無効化・再発行・公開停止によって共有権限を変更する。
  • 限定公開のお気に入りは保存時のハッシュと現在のハッシュを照合する。古いリンクの失効後は、お気に入りからも新たに閲覧できない。
  • 登録後に共有作品へ戻る文脈をアプリで保持する。ブラウザ側は作品紹介とアプリへの入口に絞り、Webの制作編集は追加しない。
  • リンクの転送先でも閲覧できる。相手が既に保存した画像や画面の回収を保証する仕組みではない。

ハマったこと・学んだこと

  • 認証UIDが決まる時点とメール確認済み会員になる時点は異なる。画像読込の再実行条件は認証UIDだけでなく会員UIDも含める必要があった。
  • 手動再読込の古い成功や失敗が、新しい保存結果を上書きしないようリクエストIDと画像revisionを確認した。
  • 画像差替の後に旧ファイル削除だけが失敗すると、ポインターを更新済みの再試行では旧パスを失う。旧ダウンロードトークンを先に失効し、非公開キューでファイル削除を再試行する構成にした。
  • 検証で使用したStorage Emulatorはダウンロードトークンのmetadata更新を既存リストへの追加として扱った。テスト専用adapterで本番の置換動作を再現し、古いトークン一つを使った実URLが拒否されることを確かめた。本番コードへEmulator専用分岐は入れていない。
  • UIKitのJPEG生成は出力寸法・色空間のEXIFを新たに生成する。EXIF辞書が空かではなく、元写真のGPSやコメントが公開画像へ残らないことを検証する。

作品へ複数カテゴリを付ける

feature未リリース

作品へ複数カテゴリを付ける

リリース状況

アプリは未リリース。ユーザーの承認により、40カテゴリ対応と続く複数カテゴリ対応のFunctionsを本番更新した。検索用の追加索引もデプロイし、準備状態はアプリ側QAに記録する。アプリ配布とサーバー更新を区別する。

何を変えたか

作品詳細と公開フォームのカテゴリを、40種類から複数選ぶ共通のチェック式一覧にした。「文字」を含む全名称を同じスクロール面で選べる。カード・詳細は選択済みカテゴリを表示し、それぞれのカテゴリの棚から探せる。最初の具体的な選択で既定の「その他」を置き換え、最後の選択を外すと「その他」へ戻す。

なぜ変えたか

花と文字の組み合わせなど、作品は一つの分類に限定できない。利用者から複数指定と「文字」が見当たらないという指摘があり、分類のデータ構造と選択導線を一緒に改善した。

技術的な内容

ローカルの編集原本を書き換えず、分類台帳へカテゴリ配列を追加した。従来の単一値も併記し、旧形式を1件の選択として読み込む。改名・削除・再読込で全分類を保持し、欠落・null・不正配列の台帳を空データとして上書きしない。

公開APIは旧category入力を受理し、新categories配列の1〜40件・重複・既知識別子・主カテゴリとの一致を検証する。カテゴリと前方一致検索を一つの配列条件にまとめるため、カテゴリIDと正規化済み検索語のprefixを組み合わせたtokenをサーバーで生成する。旧投稿の検索条件と新tokenをORでまとめ、ソートとページングを一つのクエリで維持する。既存投稿の一括書換は行わない。

ハマったこと・学んだこと

  • 単一選択を配列へ変更する際、保存だけ直しても棚の検索や公開入力が単一値のままなら利用できない。表示・草稿・検索・索引まで境界ごとに確認する。
  • Firestoreの検索tokenは、文書サイズ・索引件数だけでなく索引の合計バイト数も検証する。今回の新tokenは必要な複合索引2件を残し、不要な単一フィールド索引を除外した。
  • SwiftのdecodeIfPresentで必須だった旧キーまで欠落許容にすると、壊れた台帳の保護を退行させる。旧必須キーはdecodeを維持し、新キーの欠落だけを互換扱いにする。
  • UIテストでは背面とシートのスクロール面を区別する。見つからない操作を大きなスワイプで探し続けると、対象を通り越して原因を誤認しやすい。

検証と今後

アプリ単体717件成功・実接続用1件skip、Functions単体31件、demo専用Emulator25件が成功。最大条件で文書JSON924,748byte、索引24,037件・合計7,957,185byteを検証した。旧投稿と新投稿の検索・新着/人気・ページング・非公開除外をEmulatorで確認した。Simulatorの結果と本番反映の証拠はアプリの docs/design/qa/2026-10-30-multiple-categories/QA.md に記録する。実ユーザーの作成・削除・確認状態変更は行っていない。

画像方式切替の親Simulator受入と独立readerの補強

feature開発中(未リリース)

画像方式切替の親Simulator受入と独立readerの補強

リリース状況

画像方式切替はPR23でmainへマージ済み(ef53783846bfbbab05cf6369f1ae06a06cab0407)。App Storeへの配布、実機Pencil、試縫い、最低iPadOS17は未確認のため、未リリースの開発記録として残す。面5方式を含む統合版は別工程で検証中。

本記録は初期の画像方式履歴メモの追補。以前の開発途中の記録を完了済みへ書き換えず、今回の固定コミットと結果を追加する。

何を変えたか

元画像を共有したPhoto/Paint/Sketch/Crossの切替を実装し、有効な編集正本を一つに保って他方式の設定・糸・マス編集を履歴へ保存した。明示切替だけschema44へ昇格し、候補取消・失敗では元の図案を保持する。

初回レビュー9d769aaから、二重正本・未知履歴field・候補の針表示を修正。共通状態の取消文言を最大文字でも全文表示し、iOS26.5と27で異なるFiles保存ボタンの識別にも対応した。コードの必須指摘6件を同じ独立レビュアーが解消確認した。

ハマったこと・学んだこと

針とjump終点が合っても、余分な命令を排除できない

初期readerはjumpが終点へ進む途中の点を許容していた。親が、期待する1回のjumpに対し、中間1unitと終点10unitの2回を持つ合成EXP8byteを実際に読み込ませたところ、一致として受理した。これは実UI出力が壊れた証拠ではなく、照合の保証範囲の不足だった。

最小分割数と各整数分割点、命令位置・順序を厳密に照合する形へ補強した。余分なzero jump・往復・中間点の1unit変異・早期END・色替えやtrimの位置変異を負対照へ追加。half-awayの丸めと、0.5直前の浮動小数境界も別に確認した。reader8テストが成功し、既存UI/派生54ファイルを再読した。親も同じ合成EXPが拒否されることを独立して確認した。

OSの表示名が同じでも、識別子は同じとは限らない

iOS26.5の親確認は、ファイル名を空にして入力し、完全一致を確認するところまで成功した。その後の保存ボタン待機で失敗した。画面とAXを突き合わせると、27では識別子を持つ同じ「保存」ボタンが、26.5では表示名だけになっていた。

Native Filesの同じnavigation barへ対象を限定し、識別子または表示名に一致する操作が一意で、画面内・有効・操作可能な場合だけ保存するようテスト手順を修正。26.5の最小EXP実保存で確認した。表示のない操作や複数候補を適当に選ぶfallbackにはしなかった。

Textだけの自然高指定ではButtonが圧縮される

最大文字の取消は、Textだけを複数行にしても、親の短い高さ提案でButton自体が44ptへ圧縮された。ButtonとTextの両方で自然高を保ち、実canvas内への包含と実取消まで確認した。最終の縦横Button高125.5ptは独立計測した全文高125.274ptを満たす。整数へ切上げた126ptとの比較によるテストの誤失敗も区別した。

親の独立受入

実装者が機能別のCore/App/E2EとReleaseを確認し、独立レビュアーと往復した後、親が別のiOS26.5 Simulatorを操作した。CUA座標操作の環境制約から、ユーザーの明示承認を得てXCTest操作へ切り替えた。

初回は白画像の失敗保持83.286秒、最大文字の縦横取消297.634秒が成功。通常ケース496.972秒は前述の保存識別で失敗し、bundle全体を成功へ読み替えなかった。修正後、同じアプリ本体へ新しいUIテストBundleを当てた通常ケースが744.582秒で完走した。UIテストが製品APIへ直接linkしないことと、実install先のアプリhashが前後一致することも確認した。

通常UIで保存した作品4はschema44、図案1、PNG1、gamma1.7、配置・trim・下絵削除・他3方式履歴を保持。実保存したPES/DST/JEF/EXPは各6600針と全命令位置順・分割点・対応糸順が一致。同じ実文書からの派生16形式も一致したが、派生を実UI出力数へ加えていない。PESのRGB、JEFの番号糸順、DST/EXPの色情報の有無を区別した。

不要になった今回作成のSimulatorは、文書・実出力・画面・AX・結果を退避して削除した。後続統合確認に必要な親端末は停止保持し、他作業の端末を削除しない運用とした。

元画像と編集履歴を保つ画像の縫い方切替

feature開発中(未リリース)

元画像と編集履歴を保つ画像の縫い方切替

リリース状況

未リリース。ローカルで実装・検証・独立レビューを進めている。初回レビューの指摘を修正中で、親のSimulator操作、最終レビュー、PR・CI・マージは未完了。App Storeでの提供を確認していない。

何を変えたか

元画像を持つ図案で、Cross/Photo/Paint/Sketchを切り替え、それぞれの編集値へ戻る仕組みを実装中。現在の編集可能な図案と、他方式の設定履歴を分離し、画像を針データへ平坦化して戻れなくする操作を避ける。

なぜ変えたか

画像から作った図案は、それぞれの方式を個別に編集できても、方式を変えると元の設定やマス編集を再利用できなかった。縫い方を比較するために画像の取込みからやり直す負担を減らす。

ユーザーにとって何が良くなるか

同じ画像と位置・大きさを使って異なる表現を試し、適用前の候補を取り消せるようになる。元画像のない手描きマスやQRは推測変換せず、利用できない理由を表示する。

技術的な内容

保存正本には有効方式を一つだけ持ち、非表示方式の編集情報を別の任意履歴へ保存する。元のPNGを共有し、履歴側からも参照を追跡して画像の回収時期を決める。旧文書に任意fieldを自動挿入せず、明示的な利用時に必要なschemaへ昇格する。

プレビューは確定した作品から分離し、保存・集計・ミシン用出力は確定正本を使う。候補の針表示、針位置マーカー、再生は同じ候補計画へ接続する修正をレビュー指摘に基づいて進めている。

ハマったこと・学んだこと

  • 任意fieldでも、古いschemaへnullを注入したデータとfieldが存在しない旧文書は区別が必要だった。値がnilかだけを見る検証では、不正な新fieldの存在を検出できない。
  • 有効方式と同じ方式の履歴を同時に受け入れると、どちらを正本にするかが二重になる。切替操作の正常系だけでなく、復号された外部データからの不正な組合せを拒否する必要がある。
  • Codableの自動復号は未知fieldを無視するため、将来の方式を含む履歴を再保存で失わないよう、未知の方式fieldを明示的に拒否する境界を検証している。
  • UIテストで親要素へ付けた識別子が子の操作識別子を置き換えた。操作ごとの識別と、画面に実際に見えている操作対象の選択を分けて修正した。
  • 最大文字の短い一覧では、ドラッグの移動量から指の遊び分が差し引かれる。短い操作を繰り返した失敗だけで到達不能と判断せず、実viewportと操作領域の包含を測り、実際のロック状態の変化まで確認した。

面縫い5方式・点入力の穴と、編集全体の予算を接続した

feature未リリース

面縫い5方式・点入力の穴と、編集全体の予算を接続した

到達点とリリース状況

Tatami・Legacy・Circular・Contour・Vector Cross12、点入力による穴編集と共有生成予算をPR24でmainへ通常マージした。mergeは9027db9afab1c61e98755b20b28d0a5c657a194c、検証済みtreeとの一致を再取得して確認した。App Store配布・実機Pencil・最低iPadOS17・試縫いは未確認なので、未リリースの開発記録とする。

先行画像方式切替PR23と、途中の面生成器メモを補う記録。過去の成功・失敗や未実装記録は残し、今回の固定sourceと独立受入の事実を追記する。アプリ内のリリース告知は掲載しない。

利用者にとって変わったこと

通常の閉じた形と穴へ5方式を選べる。Vector Crossの12模様は1カテゴリ内の選択肢。候補を確認して取消・適用し、外形と穴を編集可能な正本として保存する。点入力で穴を作成でき、Undo/Redo、保存再開、ミシン用ファイル出力までつながった。

Pencilの輪郭入力と生成された針を区別し、方式変更の副作用で形を針列へ平坦化しない。旧nilの「これまでの面埋め」は、開いただけで新方式へ移さない。

生成器の上限だけでは一回の編集を制限できなかった

初期のpreviewと穴では予算を共有していたが、設定リセット・貼付・変形・文字/SVG導入等にはtransactionの後で別Builderを始める入口が残っていた。局所stageがそれぞれ成功しても、編集全体は上限を超え得た。

新方式を含む作品では選択外とproposedも対象にし、準備・検証・曲線近似・画像解析・導入・全planを同じrequestの残額へ通した。キャッシュからの再利用にも対応するworkを課金し、失敗・取消でも消費を戻さない。成功したplanを正本と一緒に採用し、採用後の再Builderを避けた。

純metadataは名前・lock等の閉じた一覧に限定し、同文書/revision/生成IDの確定planを再利用する。糸・可視性・順序・形状は出力変更として扱う。Undo/Redoは履歴targetを生成してから採用し、生成失敗で履歴cursorや正本を先に動かさない。

入力取得と形状計算の境界をコードで確かめる

外部bytes/PNG decode/OS輪郭取得は既存の有界入力段階として維持した。一方、LetteringRendererとSVG parseは戻る前に曲線flattenを実施していた。型付きgeometryを返すという理由で除外せず、実UIからsource/Dataを渡すsession domain APIへ分離し、flattenと導入以降を共有した。QRもOSのL8画像を得た後のpixel→cell変換を共有した。

同じ監査でPhoto/Paint/Sketch、raster量子化・輪郭、MagicWand、画像Cross、特殊サテン内の候補Builderまで確認した。旧nil APIと旧出力を保ち、Appの旧parse error/transaction errorチャネルも回帰した。

境界は独立した必要量で検証する

Boolean分解のsort漏れは比較予算を事前予約し、明示mergeへ接続した。大mergeには256要素ごとの取消確認を追加。SVGのline/polyline/polygonは正規化に失敗すると後課金へ到達せず0消費だったため、導入・変換直前の課金へ変更した。

独立literal20/30/34のexact/1不足、同点失敗4/6/6、1024点mapの512途中取消・512work保持を確認した。小さなfixtureの独立oracleと、複雑な実データの補助測定を区別する。

共有化後のSketch128更新は実際に2mを超えた。同じproposed解析451681workをedit2/commit2/final3で呼ぶ下限3161767を示し、128は正本/Undo/plan保持の負例へ。64brightness更新を正例にしてPhoto/Paint128と旧nil128も保ち、予算を緩めなかった。

機能別検証と親の独立確認

実装者が機能別unit/E2Eを実行し、独立レビュアは同じ1体を継続。最終Core1231件、App815件(実Firebase opt-in1件skip)は失敗0、generic Simulator arm64 Release buildも成功した。各source段階の件数を合算して一つのrunとは呼ばない。

親は別のiPad Simulator26.5へ検証済みsnapshotをコピーし、app177ファイルを前/中/後で一致確認。指定1caseを859.685秒で完走し、5方式39/39/66/34/99針、4tap穴、Undo/Redo、保存再起動、実PESを確認した。画面8枚とAX、実保存schema43/1図案/Circular/穴4点も親が確認した。

親の実PESは83針・93命令・1444bytes。保存正本から同Coreで作った期待計画に対し、strictな独立readerで全座標・順序・jump最小分割/中間点・trim/color/stop/endとRGB糸順が一致。CLI派生4形式は別出典として一致し、JEFは元planの糸番号、DST/EXPは色情報非保持を区別した。STOP0fixtureの限界も明記した。穴内部非交差はraw計画83stitch segmentをFraction区間oracleと人工横断で別に確認した。

型付きTatamiを含むCross/QR、領域分割、数値変形の各E2Eも個別成功。UI2の旧入口trace食違いは原因未確定の失敗記録を維持し、同source/binaryの固定相対xctestrun単独成功と区分した。全UIカタログ一括成功とは表記しない。親の実ファイルは自app Documents、子の一部はAppGroup内にあり、回収先の固定仮定も訂正した。

必要な実文書・実出力・画像/AX/結果を退避して、今回作成した13台のSimulatorは全て削除。親が一覧とdevice directoryの不在を確認し、他作業端末は変更していない。

続く課題

Guided/Gradient/Tartan/Meander、Stipplingの品質と文書/UI製品化、上流全詳細との同等性は未完。5/9を9/9とは数えない。Cross coverageのnextUp/down・4096bit境界・途中取消の追加独立oracleは任意保守課題として残した。既存exact判定・上限・取消保証は実装/回帰済みで、追加試験未実施と区別する。

前形式の完了通知を残すと次の出力button下端10ptがviewportから外れるUX制約も未修正。通常の一意「閉じる」操作後の実4形式保存は確認したが、製品UI修正済みとは記録しない。

一次証拠はアプリのdocs/design/qa/2026-10-30-circular-contour-delivery/COMBINED.mdとparent-acceptance/QA.md。製品source3b562b0、最終固定8636957に対する親履歴6523622582/同reviewer remote最終6523659044を確認して通常mergeした。App PR24にはchecks/status/workflow登録がなく、CI成功とは称さない。知見repo側のCIとアプリ配布も別に扱う。

円形螺旋と等距離輪郭を分けて生成する面縫い

feature開発中(未リリース)

円形螺旋と等距離輪郭を分けて生成する面縫い

リリース状況

未リリース。独立した生成処理はローカルの単体テストと独立レビューを通過した。保存・変形・編集UIへの接続は実装中で、機能全体の完成、Simulator受入、PR・CI・マージ、App Store提供を意味しない。

何を変えたか

Circularは一定の半径増分を持つArchimedes螺旋を領域で切り抜く。Contourは輪郭を等距離で内側へ移した層を生成する。既存の相似縮小によるRowFillは別の生成方法として維持する。

Contourには内から外、外から内の螺旋、往復する二重螺旋の3策略を用意する。穴やくびれで領域が分かれる場合は、全ての枝を扱う。安全に接続できない箇所を無言で別の策略へ置き換えない。

なぜ変えたか

中心からの相似縮小は、長方形や穴付き図形で輪郭から一定距離になるとは限らない。円形螺旋と輪郭沿いの面縫いを、それぞれ異なる幾何として試せるようにする。

ユーザーにとって何が良くなるか

図形の特徴に合った面の縫い方を選べるよう、編集可能な形状と方式設定を保存する接続を進めている。実機の縫い品質や全入力で一本の螺旋になることは保証しない。

技術的な内容

  • Circularの自動中心は外周から穴の面積モーメントを差し引く重心。輪郭の点順を反転しても穴の役割を変えない。
  • Bezier正本を許容誤差内で導出した点列へ近似し、螺旋の切抜き、offset、接続候補、安全判定、反復と同じ残処理量を使う。
  • Contourはネイティブのパス演算を使う前に保守的な処理量を予約し、出力の点数・層数・自己交差を検証する。ネイティブ処理内部の即時取消や壁時計時間を保証するものではない。
  • 二重螺旋の継ぎ目は、実接続距離と弧長順に候補を試す。後の周回と両立しなければ前の層へ戻る。失敗した探索の処理量も消費したままにし、予算不足を通常の接続不能へ変換しない。

ハマったこと・学んだこと

次の線だけ安全でも、後の周回線と交差する

二重螺旋は層0から層2へ進み、復路で層1へ戻る。このとき接続線と現在の線だけを比較しても、後から追加する層1の周回との交差を防げない。追加する周回の全辺を既存の全runと照合し、針長分割後の実際の線分も別runを含めて最終検証する形へ修正した。正長の重なりとT字接触も負例に含めた。

閉ループの終点を再計算すると、同じ始点にならない

弧長へ周長を加えて終点を再補間したところ、約1e-14のずれで接続線との微小なT字交差が生じた。閉じた周回は保存した同一始点へ戻す。交差判定を緩めて解決したものではない。

境界の交点は数値上わずかに面外へ丸められる

曲線の切抜きでごく短い面外区間が生じる負例が見つかった。切断した端だけ表現可能な内側へ16座標ULP分動かし、補正の上限と厳密な全線分検査を残した。切断していない端点は再補間せず元の値を保持する。製品の内外判定を使わない独立した符号計算で、補正を外すと面外端点を検出できる負対照も確認した。

刺しゅうの設定を見本から選ぶ共通UIへ揃える

ux未リリース

刺しゅうの設定を見本から選ぶ共通UIへ揃える

リリース状況

未リリース。ローカルのiPadOS Simulatorで検証し、PR #16をmainへマージ済み(a23efddb45613508085ff3fc7c82fb4e00099ab7)。本番配布や試縫いの成功を意味しない。

何を変えたか

4タブへ設定を移動した際に文字メニュー中心になった縫い方の選択を、模式図と日常語の見本へ戻す。2列のタイル、選択チェック、6色の色見本、数値の直接入力と大きな-/+を共有し、素材・図形・線・描き方・縫い方・画像変換・確認・設定の選択へ適用する。

DESIGN.mdは現在の規約として書き直し、以前の配置は別の履歴文書へ保存する。ユーザーの参考画像と表現の使い分けも正本へ含める。

なぜ変えたか

項目を同じ面に集約しても、方式名を文字だけで選ぶフォームでは線や面の違いが伝わりにくかった。ユーザーが求めたのは、線の見本・色・数値を見ながら選ぶ操作だったため。

ユーザーにとって何が良くなったか

細い線・重ねた線・幅のある線・面・縫わない状態を見分けられる。色番号を知らなくても基本色を選べ、数値はキーボードを開かず少しずつ調整できる。直接入力や細部の設定も同じ面に残す。

技術的な内容

SwiftUIのBindingを受け取る見本タイル、選択肢の短い行、色見本、数値部品を追加する。作成時は作成草稿、既存図案は対象別の未適用草稿へ接続し、表示の改善で正本への適用経路を複製しない。

横画面は右の縦スクロール、縦画面は上部3段目の横スクロールという既存の配置を維持する。大きなグリッドを一つの背の高いViewにせず、短い2列の行をLayoutへ渡し、縦画面でも高さ制限内で横へ送る。

判断したこと・トレードオフ

  • 選択の形に意味がある縫い方・図形・線には模式図を使う。機能入口には記号と日本語名を併記する。
  • 可変のオブジェクト名・糸名・点番号、本文やURLまで無理に画像へ置き換えない。
  • 見本は方式の説明であり、実際の生成結果や縫製品質の保証にしない。
  • 上部の現在値は確定済みのモデルから表示し、見本の選択だけで適用済みとはしない。

ハマったこと・学んだこと

共通の数値ステッパーでDoubleをそのまま文字列にすると、整数欄の20を増やした結果が 21.0 になる。既存の Int パーサーが拒否するため、UIの外観だけの変更でも入力境界が変わる。整数となる結果は整数表記にし、列数を増やしてグリッド作成まで進む受入で確認する。

数値TextFieldとStepperを同じDoubleへ直接結ぶと、文字欄が「-」でも最後の有効値から増減できてしまう。数値を文字列草稿として保持し、有限値へ解釈できる場合だけプレビューを更新する。解釈できない間は増減と適用を抑止し、モデルの適用経路でも再検証する。対象別の草稿と、現在の方式では使わない項目の扱いを分ける。

図形の見本も、名前が似たSF Symbolを選ぶだけでは正しい形にならない。角丸四角に上角だけの記号を使っていたため、四隅を丸めるコード描画へ修正した。固定の選択肢を棚卸しし、配色関係・文字アーチ・レール・領域別の縫い方も見本に揃えた。

入力保持や生成取消、最近使った糸などは、見本を共通化するときも元のモデル・Binding・状態表示へ接続する必要がある。見た目が同じでも、草稿の編集と即時適用の部品はそのまま交換できない。

作業中にアトリエホームがmainへ統合されたため、起動を新しいホーム用の共通テストhelperへ接続して再検証した。設計規約の全面改稿では、古い配置を退避する一方、別の変更で承認・実装されたアトリエと認証の境界を消さずに取り込む必要があった。

編集画面の通常ボタンを透明にする

ux未リリース

編集画面の通常ボタンを透明にする

リリース状況

未リリース。PR19でmainへマージ済み(b6fcbe00604c4c9b0d0b1b15b391bd5b2c2920d5)。通常の文字・アイコン操作ボタンの外観調整。

何を変えたか

共通のEditorActionButtonStyleとWorkspaceIconButtonStyleから薄い青背景と枠線を除き、透明にする。押下は文字・記号のopacityで示し、無効状態と破壊操作の赤色は保持する。DESIGN.mdも同じ規則へ更新する。

なぜ変えたか

利用者から薄い青いボタンを透明にしたいという要望があった。通常操作を見分けるための文字・アイコンと、入力欄の枠を使い分け、背景で囲む表現を減らす。

ユーザーにとって何が良くなったか

名前変更、ツールバー、ゴミ箱などの操作を、薄青の塗りや枠線で囲まない落ち着いた表示にする。

技術的な内容・学んだこと

ボタンの背景を外すと、透明な余白まで押せる領域として扱われるとは限らない。44ptのframeに加えてcontentShape(Rectangle())を明示し、表示と操作領域を分離する。既存の名称・操作・配置を変えず、共通styleで反映する。

専用図案のプレビューと公開画像を実際の縫い線から描く

bugfix未リリース

専用図案のプレビューと公開画像を実際の縫い線から描く

リリース状況

未リリース。独立レビューで必須指摘を解消し、PR #15をmainへ統合した。ローカルの単体テスト・実接続・最新4タブとのUI確認を終えた段階。ストア配信・実機Pencil・試縫いの成功は確認していない。

何を変えたか

作品棚とFirebaseへの公開JPEGで、クロス・Pixel・QR・Photo・Paint・Sketchの内容を描画するよう修正した。色数は専用パレットのうち実際に使う色から数える。図案のアフィン配置、空白マス、別々の縫い線をつなぐジャンプ、色の上書きを維持し、編集原本は平坦化しない。

なぜ変えたか

独立レビューで、汎用の輪郭パスを使うプレビューでは専用図案の外接四角形しか描けないことが分かった。ログインと画像送信の成功だけでは、送信した画像が実作品を表しているか保証できなかった。

ユーザーにとって何が良くなったか

手元の棚や公開した作品紹介で、図案の中身と実使用色数を確認できる。作成・保存・再開はオフラインのままで、公開時に送るのは表示用の派生画像と紹介情報だけに保つ。

技術的な内容

既存の検証済み・処理量上限付き生成器を表示用にも使う。クロスは有効マスと使用パレット別のrun、写真系は生成結果のlayer別runを描く。空白をまたいで線を追加しない。生成失敗時は外接枠で成功を装わず、プレビューを利用不可にして原本を残す。

ハマったこと・学んだこと

プレビュー用テストには、外枠の有無だけでなく内部の画素と色、空白の領域を検証する必要があった。JPEGの圧縮・アンチエイリアスを考慮して範囲で測定し、隣マスの線が測定範囲へ混ざらないようにした。

実接続のfixtureも手作りの任意JPEGから、実制作と同じEditorSessionで保存した2色Pixel図案へ変更した。手動のモデル構築でPixel用の保存スキーマ更新を忘れた失敗から、テストデータも制作経路を通す重要性を確認できた。

検証メモ

  • 2色クロス/Pixelと写真系3方式の画素テストは、修正前に失敗することを確認した後、修正後に成功。
  • 最新mainの4タブ化も取り込み、Core995件、App unit 621件(実接続用1件skip)、失敗0。
  • 最新4タブ統合版のUI7件成功。旧装飾メニューの通しテストはSimulatorのアニメーション待機が反復して中断し、未完了として別記した。変更したホーム・点入力・保存再開の直接の代表テストは91.033秒で成功した。
  • 既存ユーザー3名の実接続確認は48.660秒・失敗0。2色のJPEG公開・画像取得・カテゴリ/タイトル検索・いいねの冪等性と件数・本人専用お気に入り・解除・非公開化・ローカル原本不変を確認。
  • 起動ホームの変更に合わせ、既存UIテストの作成・棚からの再開を共有helperへ統一。画像取込と書き出し先選択のOS Filesは維持する。

線の反復と特殊サテンの設定を編集可能な正本へ追加する

feature未リリース

線の反復と特殊サテンの設定を編集可能な正本へ追加する

リリース状況

未リリース。実装PR18は、最新mainの4タブ・見本タイル・アコーディオン・透明ボタン・名前直接編集と文字サテン(PR17/19/20)を統合した。製品464e6b83(App7c09b9b/Core9b26a7e)のCore1056件は失敗0、App705件は既存の実接続専用1skip以外に失敗なし(15.374秒)。独立レビューで見つけた最大文字の情報重複や固定操作の開閉不一致も修正した。最新UIの必要11導線を分割確認し、Releaseのarm64/x86_64も成功した。PR18はmainへ実マージ済み。GitHubのmerged=trueとmerge commit 9b44b6f9779c4fec5400a2a85e2509e1446a127f を再取得して確認した。本番配布は未確認である。

以下は各時点の検証・判断の履歴であり、過去sourceの成功を最新sourceの成功へ読み替えない。UI56では最新mainの名前・現在値・portrait適用・図案一覧と、文字反復/既存サテンの草稿・候補取消の2ケースが成功した。UI40で実保存した65/390針の文書を最新Coreで再生成し、旧期待針列の全座標/順序/色block/stopが変わらないことと、4形式の独立reader一致を確認した。旧UI実PESと最新CLI PESも同じSHAとなった。最新UIからの実出力は後述する別の検証範囲である。

目的と範囲

ユーザーの要望により、縫い方の種類と細かな設定を優先する。線全体の通過回数とひと針区間の奇数通過周期、E/S/Zigzagの実針、基準側・送り、中心下縫いの独立針長・通過回数が今回の承認範囲。輪郭/Zigzag下縫いと他高度設定は後続へ残す。

参照先はユーザー確認済みのInk/Stitch。公式資料と固定コミットを機能参考として調べたが、GPLコード・素材・書体・サンプルの転用や翻訳実装は行っていない。針単位の互換を宣言しない。

設計で判断したこと

  • 「往復回数」では片道と往復が混同されるため「線全体を通る回数」とした。1回は片道、2回は行って戻る。各区間は奇数回を指定し、追加の補助移動なしで次へ進める。
  • 全体反復の帰路は生成済みの基準針列の厳密逆順。図案の向きを反転する編集は針を再サンプリングするため、物理区間の位相を保持すると保証しない。複数文字輪郭もそれぞれ全反復を終えてから次へ移る。
  • サテンの新方式は左右断面と送りを定義し、方式名だけを変えた同じ針列にしない。通常のnil設定は旧生成を維持する。
  • 本縫いと下縫いの密度を分ける。旧中心下縫いbooleanと新詳細の二重有効化を避け、値を無効状態でも保持する。
  • 生成針を編集正本へ置換せず、曲線・レール・実測筆圧を保持する。保存形式の昇格とclipboardを両機能の共存試験で確認する。

検証で学んだこと

  • 新AppテストはUIを通らずモデルを生成する。初期描画モードを編集へ切り替え、必要な初期生成の完了を条件待機しないと、ガードが編集を拒否して機能失敗と誤認する。
  • Undo後の草稿全体を比較すると、Redo用futureや選択状態など内部履歴の差も拾う。形・曲線・スタイル・筆圧など利用者に見える正本とRedo可能性を検証する。
  • 常設フォームには非表示の無効フォームも存在する。同じAX識別子を全体検索せず、表示中・有効の作業面へ限定して操作する。

独立レビューから増えた検証

生成の針数や保存だけが成功しても、狙った経路を縫うことまでは保証できない。不等速の2本のレールでZigzagを作ると、両端の断面と幅比が同時に変わるため、その間は曲線になる。端点の直線を細かく分けるだけでは元の曲線には戻らない。独立した手計算例では0.25mmの差が見つかった。

修正では連続した経路を適応近似し、同じ弧長から針を選ぶ。近似に使った全節点を無条件に実針へ変えるのではなく、最大針長と曲線からの誤差、帯内性を満たす点を採用する。同じ形を2節点と21節点で渡したときの針数・座標一致も調べ、内部の分割数を利用者の密度へ混ぜない。

SM3: 表示位置と進行原点を別の座標として渡す

羽毛で左右の幅が非対称になると、実際の帯の中点が羽毛前の中心線からずれる。帯の中点を矢印の表示位置にも方向計算の原点にも使うと、短い水平線でも矢印が幅方向へ傾く。0.2mmの水平線に片側羽毛を付けた独立fixtureでは、旧原点による方向ベクトルへ約0.26149mmのY差が混ざった。左右両側へ異なる量を適用する羽毛と、複合Bezierの最初の0.001mm区間でも確認した。

Coreの表示用ヒントを4座標に分けた。leftとrightは実際に生成する帯の端、progressOriginは羽毛前の開始中心断面、forwardは同じ基準で最初に進む有効な次点である。中心線のoffsetは進行原点にも反映し、羽毛による端の偏りを方向へ混ぜない。

表示アンカー = (left + right) / 2
進行方向     = forward - progressOrigin

4座標をそれぞれ有限値と座標上限で検査する。ヒントのために全実針や全羽毛stationを生成せず、生成と同じ曲線分割と断面評価を、少数の隣接点だけ保持して使う。短線の接線は、元形状に基づく独立した水平の期待値で確認した。

SM4: 画面の左右と保存したレールの識別を分ける

canvasはY軸を反転せず、正のscaleで作品座標を描く。Yが下向きの座標で法線(-dy, dx)を使うと、+X進行では画面下へ向く。この既存の内部L/Rを、そのまま「進む向きの左・右」と表示すると意味が逆になる。

中心線から作る新E/S/Zigzagでは、次の独立期待を置いた。

中心線の進行 利用者にとって左になる位置
右へ進む(+X) 上(Yが小さい)
左へ進む(-X) 下(Yが大きい)
下へ進む(+Y) 右(Xが大きい)

中心線の新方式では、利用者の左を内部R、右を内部Lへ写像する。共通の写像を実針の基準側と表示ヒントへ適用し、Eの背骨、S/Zigzagの開始側、UIで選んだ基準側を一致させた。閉線でも内外へ読み替えず、鏡映後は変形後の進行を基準にする。

直2レール式のL/Rは保存された識別を表すため、同じ交換を行わない。鏡映でも保存したレールの識別を保つ。中心線の旧法線、保存済みの羽毛・offsetの生値、通常nilと明示normalの本縫いは維持した。新しい基準側の意味を直すために、過去の文書全体を変換する必要はない。SM3のprogressOriginとforwardも左右の入替対象にしない。

C0急角: 上限で拒否する例を成功例と分けて残す

左右の意味を検査している途中で、幅に対して短い急角の例が見つかった。正本は(20,20)から(20.4,20)へ0.4mm進む水平区間に、制御点(23,16)、(27,16)、終点(30,20)のcubicを接続したもの。接線が不連続なC0接続で、サテン幅は2mm、Eの送りは2mm、最大針長は10mmである。

この入力で新Eの中心線基準左、すなわち内部Rを選ぶ候補はlimitExceededで拒否される。共有work上限は2,000,000で、現在の利用者向け理由は「データ量が上限を超えています。」。候補の失敗時にはBezier正本、作品、Undo/Redo履歴を保つ。元のC0入力を拒否回帰として残し、左右の意味は接線が連続したC1の曲線で別に検査した。当該C0 fixtureの旧nilと明示normalは同じ78針で一致し、通常経路を悪化させていないことも確認した。

同じ独立reviewerによる限定判定は、共有上限でのこの拒否が仕様内というもの。「任意の急角に対応済み」「この形を新方式で必ず縫える」とは説明しない。具体座標、元の拒否ログ、正本・履歴保持の回帰は、アプリの docs/design/qa/2026-10-29-satin-methods/CORE-C0-BOUNDARY.md に残した。

コードの独立レビューと統合版の最終受入は分けて扱う。生成・表示・保存・通常UI・別readerの証拠をそろえ、最終の受入結果をQAへ記録する。

別作業のUI再設計へ合わせた統合

最終取得でmainに4タブとアトリエホームが入っていたため、旧6分類での操作成功を最終受入へ流用せず、新しいホーム・同じ編集面・縦画面の短い横スクロール列へ合わせた。新UIの共通スクロールに、従来の独立List画面をそのまま入れると高さの計算が競合する。入力Fieldsと適用・取消Commandsを分離し、共通の作業面と固定操作領域へそれぞれ配置した。

固定操作の親Groupに識別子を付けると、子の適用ボタンへ親識別子が伝わり、自動操作や読み上げの識別が壊れる例を実UIで検出した。実コンテナとchildren containで親子の識別を分けた。見た目にボタンがあることだけで、同じ操作へ安定して到達できるとは限らない。

上部の現在値も、新しい中心下縫い詳細より古いbooleanを優先していた。実Viewと共有する値helperを旧表示のまま抽出し、8テスト29assertの実行時失敗を確認して修正した。新方式では送り、通常サテンでは間隔、明示した中心下縫いはON/OFF・長さ・通過回数、線反復は実効周期を表示する。確定済み選択はselectedObject、作成中は実草稿を読み、未適用の選択設定や一時previewと混ぜない。各時点の確認済み件数と通常UIの証拠は履歴として保持する。最新の確定結果は先頭と末尾に記録する。

最新mainの視覚部品と数値草稿を合流する

a23efddb の共通部品へ合わせ、サテン方式は既存4模式図と日常語の WorkspaceChoicePicker タイルから直接選ぶ。基準側も模式図・名前・選択状態を併記し、中心線の進行基準と保存レールの識別を区別した。送り、本縫いの最大針長、中心下縫いの独立針長と通過回数は WorkspaceNumberField へ接続し、直接入力と大きな増減操作が同じ文字列草稿を更新する。途中の不正文字を有効値へ黙って戻さず、既存の値域検証、適用・取消・Undo、58ptの固定主操作を維持した。

合流時には、画面で非表示にした旧サテン間隔の不正文字が、新Eと明示中心下縫いへ切り替えても数値検証を塞ぐ経路があった。5assertの実行時赤を確認し、実際にその間隔を消費する通常方式、または新中心詳細がない従来中心下縫いの場合だけ、表示とparseの対象にする共通条件へ修正した。非表示の草稿文字は破棄せず、未確定のまま通常方式へ戻ると不正文字が再び現れ、適用を拒否する。反復K2と周期・針長数値、新Eと引き補正数値の同時適用、不正入力の保持と修正後の生成、保存正本と未適用草稿の分離もモデルの実操作で確認した。

SM7: 方向ヒントにも数値草稿のsnapshotが必要

数値草稿が導入されても、方向requestがstyleとサテン詳細だけを比較すると、引き補正やoffsetに「-」を入力した際にstyleが変化せず、旧ヒントが未適用草稿の方向として残る。選択設定と新規描画の両方で、0.20から「-」、0.50へ直す独立テストを追加し、旧requestの同一性・非throw・幾何不変による28assertの実行時赤を確認した。

修正では PathNumericDraft の値をrequestへcaptureし、実適用と同じ SatinExpressiveDraft → PathNumericDraft の順でstyleを解決する。入力文字だけの変更でもrequestが別値になり、旧ヒントを非表示にする。不正文字は解決時に理由付きで失敗し、修正後の新requestで断面を再計算する。取得済みrequestは後の入力へ追従せず、作品・描画草稿・revision・Undo/Redoを変えない。非同期へ渡す数値草稿と内部Entryには検査付き Sendable を付け、@unchecked へ逃がさなかった。新2テストは0.052秒で成功し、App final15の676件・skip1・失敗0にも含まれる。

視覚操作への移行では、非表示フォームの同一識別子、数値欄のフォーカス、慣性で移動する短い横面、固定操作を除外した可視領域の判定を個別に確認した。文字置換はnative欄の中央tapと既存文字数のDeleteを使い、値の完全一致を確認する。到達条件の完全表示・hittabilityは保持した。

SM8: 上部へ移した操作にも独立closeを維持する

最大文字では右の設定面が上部3段目へ移る。既存実装のcloseは狭い右面のheaderにだけあり、上部では周期1/3/5を設定しても独立して閉じられなかった。visual22の実失敗から、本文右端44ptを予約し同じ高さ内へ実44×44ptのcloseを配置した。追加headerで上部を伸ばさず、本文・固定適用へ重ならない。visual23では239.746秒で閉再開と周期保持、実58pt以上の適用、95針→Undo33針→Redo95針を確認し、同じ独立担当がコードと画像/AXの整合を解消判定した。

SM9: 共通アコーディオンへ可用寸法と環境を渡す

PR17の新UIを統合すると、外側は最大文字に合わせて列を広げても、アコーディオン内側だけ280ptへ戻る経路があった。実UIの見出しは188.5pt高、本文は80ptで、スクロールでは全体表示できない。rootで解決した列幅と高さを共通の環境値として渡し、内側も同じ寸法で列を組むようにした。さらに、UIHostingControllerで隔てたフォームへも寸法とcompact環境を明示転送する。SwiftUIの親へ設定した値が別のhosting rootへ自動で届くとは扱わない。

幅を直しても、最低84pt、最大文字112.5ptの方式タイルは55〜80ptの本文へ入らない。先行の最大文字テストで再現し、広幅では同じフォームと固定操作をAnyLayoutで左右へ組み、上部の高さを増やさず本文160ptを確保した。方式は模式図と名称の横配置、数値は名称・単位・入力・増減の横配置へ適応し、文字や操作領域を縮めない。portraitでは同じcompact操作を一行へ組み、本文を確保する。処理・失敗・候補の状態は通常の固定操作かcompactのcanvas共通位置の一方だけへ置く。

実UIの方式タイル84pt/本文160ptまで確認できたが、数値欄だけを中心へ寄せる試験では名前が画面外へ出た。完全表示のassertを外すのでなく、名称・入力・増減のunionをスクロール対象にした。書体の非対象設定は実際のアコーディオン操作で閉じ、入力を削らず対象へ進む。当該時点では縦横回転・固定3操作・閉再開・取消・適用/Undoを再検証対象にした。

UI38では最大文字の方式・数値行・固定3操作、縦横回転、閉再開、取消・適用・Undoが944.157秒で成功した。ただし画像の目視から、入力装飾と現在値の重なりが新たに見つかった。フレームの完全包含やAX値だけで、表示の読みやすさまで成功扱いにはしない。

SM10: 入力装飾も拡大した実幅を確保する

数値欄の「123」を固定38ptの左余白へ上乗せすると、最大文字では装飾幅が約79.74ptまで拡大し、実値「2」に重なった。文字欄のAaでも同じ問題がある。実UIWindowに置いたUITextFieldの文字領域をhost座標へ変換し、通常/最大文字、Aa/123、短い値・複数桁・不正途中値等の12組を実フォント幅+8pt間隔と照合した。先行テストでは9assertが失敗した。

装飾をHStackへ移して実幅を予約し、実際のTextFieldとBinding、数値入力の意味を維持した。修正後の文字開始位置は最大文字のAaで71.5pt、123で98ptとなり、装飾と8pt以上離れている。隣接する±も拡大フォントでは44pt幅を越えて接触したため、共通iconと同じ20pt記号・44pt操作領域に揃えた。先行テストと操作欄の自然サイズ3件はすべて成功。同じ独立担当はコード解消と判定し、その後の実画像と入力操作の受入は、対象sourceを区別してQAへ記録した。

候補の経路と再生も同じ計画から読む

独立レビューでは、canvasの候補経路だけ新しくなり、赤い針位置マーカーと再生範囲は旧計画を使う差も見つかった。マーカーは停止中も表示されるため、再生していない場合も無害とは言えない。表示中の計画を一箇所で選び、作品・編集版・入力世代・選択・草稿が一致する候補だけを採用して、canvas・マーカー・再生範囲へ共有した。針数が同じでも座標が異なる候補では、計画全体の変更で再生位置を更新する。

一方、作品の保存・書き出し・文書針数・統計は確定済み正本を対象とする。候補へ暗黙に切り替えたり、針表示OFFで作品の針数を0にしたりしない。共通canvasには「未適用のサテン候補: X針/確定済みの作品: Y針」、確認タブの統計には「集計は確定済みの作品」と表示し、読み上げでも対象を区別する。候補と作品が同じ針数でも未適用であることを明示する。候補の表示OFFや変形プレビュー中はcanvas上の対象表示も隠し、古い候補を表示経路へ残さない。コード上で権威を分けることに加え、隣り合う情報の対象も画面で伝えるようにした。

固定操作領域の説明は、候補の存在とcanvasへの表示を別に扱う。「未適用のサテン候補: X針」だけを伝え、有効候補がある間は表示OFFや変形中でも説明を保持する。raw outputから「キャンバスに変更後の針を表示しています」と断定すると、表示状態や失効と食い違う。実View共通helperを旧動作のまま抽出し、既存8テストに期待を追加して14assertの実行時失敗を確認した後、表示計画と共通のsnapshot検証へ切り替えた。文書・revision・入力世代・source・選択・草稿の不一致、取消、失効後は説明を消す。この修正は最新App final15の676件・skip1・失敗0でも回帰確認した。

確認済みの保存・出力

旧UI40の保存・出力ケースでは、E・右基準・送り1.2mm・中心下縫い3.5mm/2通過を保存し、アプリ再起動後の設定と390針を確認した(507.513秒)。線反復の2通過も、Undo/Redo・保存再起動後の65針と設定を確認した(108.527秒)。同じ実行束に含まれる他ケースの結果とは分けて記録する。

この実UIで保存したPESは2030bytes、SHA256 6cb4e38176191095eb1a015516b7ea479f136d8756a4c4fde46a654ae992f68c。pyembroidery 1.5.1で全390針の順序・座標・色block・黒RGB・stop0/end1を照合し、同じCoreから生成したCLI PESともbyte一致した。保存文書からのCLI出力はPES/JEF/DST/EXPの4形式を独立readerで照合し、線反復の保存文書も65針で一致した。UIからのミシンファイル保存を確認したのはサテンPESであり、他形式のCLI照合とは区別する。DST/EXPのRGB非保持とJEFのtimestamp差も、全形式のbyte一致という説明へ置き換えない。原本・期待針列・reader結果・由来とSHAはアプリQAの ui40-output に記録した。

確認の限界

テスト・UI・Release・保存・出力はそれぞれのsourceでQAへ記録し、単一bundle全件成功とは言い換えない。物理Pencil、最低iPadOS17、試縫い、全VoiceOver/ダークの成功は未確認。未配布なので通常記事・SNS・アプリ告知は追加しない。

一次資料

SM11: 失敗の文章を図形と分け、現在の入力へ結び付ける

キーボードと最大文字が同時に使われた画像で、方向表示の失敗文が既存の操作説明へ重なっていた。図形overlayから文章を外し、失敗時だけ操作説明・復旧情報と共通の高さを制限した縦スクロールへ集約した。上と下の情報領域の合計は、画面全高でなく実際にcanvasへ割り当てられた高さ以内にする。最大文字ではfootnote相対の拡大率に従う1行高と上下16ptの余白を優先し、headerを残りへ制約する。エラー本文はfootnoteで短縮せず、長い内容の末尾までscrollで到達させる。

表示状態は対象・入力snapshotと一緒に保存し、現在のrequestに一致する結果だけを受け取る。入力の変更直後に古い理由を隠し、古い成功やclearが新しい失敗を消すことも防ぐ。この状態は編集正本やUndoへ入れない。実モデルの入力変更・遅延結果・正本不変、実UIScrollViewの高さとcontent overflowを新たに検証した。

実画面の切り取りとnative AX外形を混同しない

この情報欄を最大文字とキーボードで確認したUI43では、native UIScrollViewのAX外形が469.5ptでも、実際の切り取りと縦スクロールバーのtrackは47.5ptだった。外形はキーボード側のsafeareaまで広がっていた。画像とtrackを突き合わせ、テストの実viewport計測を直した。同じreviewerは47.5ptが実1行52.5078ptより低いことも指摘した。実UIFontの1行と余白を要求する先行赤から最低高を修正し、UIも最後の1行の帯全体が入ることへ強化した。修正後の全文末尾への実scrollと正常復帰は別に検証し、見えない場所があることを測定法だけで成功へ読み替えない。

途中では別のinstalled binaryが、テスト対象Coreに存在しないinitializerを要求する起動失敗もあった。クラッシュしたdebug dylibとDerivedDataのUUIDが異なることを確認し、インストール元を推測せず、専用SimulatorとDerivedDataへ隔離した。開始と終了の実installed executable/debug dylibをUUIDとSHAで照合し、検証対象sourceと実行成果物が一致することを記録した。

SM12/13: 情報の対象表示と物理方向を同じ場所に重ねない

候補を示す白帯は針数の誤認を防いでも、canvas上の進行矢印や左右の文字を覆うことがあった。既存headerへ候補/確定情報を移し、候補の出現と取消でcanvasの寸法を変えないようにした。通常は既存高で横scroll、文字拡大は高さを制限した縦scrollを使い、全文とUndoへ到達できる。

方向の文字も固定offsetでは拡大時に重なる。実際のText寸法を計測し、canvas内の決定的な有限候補から互いに離れる場所を探す。物理矢印と左右railを動かさず、移した文字だけ細いleaderで実位置へ結び付ける。未計測や安全配置が見つからない間は文字を隠して全文の理由をheaderへ出す。有限候補の失敗は、数学的に配置不可能だという証明ではない。表示の状態は現在requestへ限定し、編集正本や数値入力の生成失敗とは分ける。

実UIFont/Hostingの先行赤と幾何検査、通常/最大文字の実UIでsource/header/canvas分離と文字同士の非重複、回転・取消・Undo後の再計測を確認した。UI48の同値E候補は441針/確定441針でApply無効、UI46の変更Apply409針/Undo524針とは異なる受入として記録する。

最新mainの同番号schemaをpayload別に検証する

別作業の文字サテンもschema41を使うため、番号が同じことだけでは相互の対応を保証できない。線反復と文字サテンは各payloadの検査で41以上を要求し、特殊サテンを含む場合は42へmax昇格する。新共存回帰では3機能の挿入/clipboard貼付の全6順序、保存復元・Undo/Redo、旧nil生成、低schema拒否を確認対象にした。最新画面への統合後の結果と実マージは先頭のリリース状況へ反映する。

SM14/15: 最新mainの初期折畳みと固定操作を一緒に扱う

mainで撤去された機能は、新規featureファイルの排他guardにだけ参照が残ることもある。統合後の実compileで配色機能の参照を検出し、その条件だけを除いて他の排他ガードを保持した。

設定本文は初期折畳みでも、別の固定footerだけ常に表示されていた。既存UIの『閉じた設定に適用はない』を先行赤で確認し、線・既存サテン・文字反復それぞれの本文と固定操作を、同じ展開状態とBindingへつないだ。本文は既存のmounted childrenを保ち、閉じて入力草稿を作り直さない。サテンの候補・生成・失敗がある間は、閉じても状態と取消だけを残す。

一覧表示では上部タブが編集のままなので、タブだけを見たfooter条件では一覧に適用が残る。実際に表示しているdetailPanelも判定へ加え、directionも隠れた草稿を編集中とは扱わない。有効候補と選択正本は表示でき、入力と非同期状態は戻った時に再開できる。実UI56では名前/現在値/portrait適用/一覧が91.559秒、文字反復と既存サテンの初期footer不存在・草稿の閉再開・候補中の取消・取消後の空footer消失・同候補再生成が270.115秒で成功した。同じ独立担当も画像/結果から解消と判定した。

最新mainでの最終検証と実行成果物の同一性

最新の必要11導線はUI56の2成功、UI57の不変body6成功、UI59の3成功に分けて確認した。単一bundle全成功ではない。UI57は8中6成功・2fixture失敗で終了した。同時点で、ディスク上のtest bundleと実際に読み込まれたrunnerのUUIDが異なることを実プロセスのsampleで検出した。実App2imageは最新製品と一致したため、body/helperが不変の6ケースについて新Appの実traceだけを採用し、未実行の追加keyboard検査を成功へ含めなかった。原因の帰属は断定しない。

runnerだけを再導入し、制作Appと文書を保持したまま専用Simulatorを再起動した。追加検査を独立selectorへ移し、実loaded test/App・installed・derivedのUUIDと終了時SHAを照合したUI59は全3成功。独立した最大文字/keyboardケース271.865秒、特殊サテンの保存・実PES415.432秒、4タブ55.630秒である。実viewportはAX外形496ptでもkeyboard本体までの129ptでもなく、画像上のscroll track74pt。理由の初行・末尾・操作説明末尾の全字形へ到達し、訂正で情報欄が消えることを画像で確認した。残高headerの全操作到達まで保証したとは扱わない。

サテンの左右labelは、物理点を動かさずleaderで接続したため、labelの上下だけを物理側の期待値へ使えない。旧midY比較を訂正し、4labelのcanvas内包・相互非重複とCoreの物理左右fixtureを維持した。4タブは新mainの初期折畳みを実際に開くfixtureへ変更した。期待値を減らすのでなく、表示文字と物理幾何、実操作の役割を分けた。

最新の実保存文書はschema41の線反復K2/C1・65針、schema42のE/右基準/送り1.2mm/中心下縫いON・3.5mm・2通過/390針。UI59で実保存したPESも2030bytes、SHA256は旧出力と同じ 6cb4e38176191095eb1a015516b7ea479f136d8756a4c4fde46a654ae992f68c で、新Core CLI PESと一致した。実PES自体と両文書のCLI4形式をpyembroidery 1.5.1で全針座標・順序・色block・stop/end照合した。証拠はアプリQAのui59-outputに保持する。UI保存はPESのみで、独立jump比較やJEF timestampのbyte一致までは主張しない。

最終製品は464e6b83、試験9652f0e、証拠固定6952a99。Core1056件失敗0(77.567秒)、App705件は既存実接続専用1skip・失敗0(15.374秒)。Release5もactual exit0、両Simulatorアーキテクチャを確認した。同じ独立reviewerが全PR1/SM1〜15を解消と判定し、ローカル受入可・必須残件なし。生成ログ抜粋の空白警告だけ任意指摘として保存し、全差分無警告とは説明しない。

留め縫いの強度と実針間の接続線を再編集できる形で追加する

feature開発中(未リリース)

留め縫いの強度と実針間の接続線を再編集できる形で追加する

リリース状況

未リリース。実装、ローカル受入、独立レビューと修正・再判定を終え、アプリPR13をmainへマージ済み(a4e11fc077b41952311af747861c8b778412d60f)。アプリ配布は未実施。物理iPad・Pencil・ミシン・試縫いの成功を示す記録ではない。最新の根拠はアプリ側の docs/design/qa/2026-10-29-locks-connections/QA.md で管理する。

何を変えたか

留め縫いにFine(各端1往復・2針)、Firm(従来の2往復・4針)、Offを追加した。始め・終わりを個別に切り替え、長さは0.1〜2mmで設定する。Offでも直前の長さと強度を保持する。

図案間の移動を編集可能な細い線へ変える操作を追加した。元の最後の実針から次の表示中の図案の最初の実針へ、針目長2.5mmの独立したRunningパスを挿入する。元の形や曲線を平坦化しない。

なぜ変えたか

留め縫いの有無だけでなく、追加する往復数を選びたい要望があった。図案の輪郭や中心から接続線を作ると、実際に縫い終わる場所とはずれる場合がある。出力用の生成計画を読みながら、追加する線は編集正本として残す必要があった。

ユーザーにとって何が良くなったか

留め方と各端の有効・無効を調整し、後から同じ長さで再開できる。接続線は通常の点・曲線・糸色・針目の編集へ進め、追加を1回のUndoで戻せる。元や接続先を動かしても自動追従しない独立した図案であることを表示する。元の糸切り・停止は保持するため、接続線を追加しただけで糸が連続すると保証しない。

技術的な内容

強度は省略可能な値として追加し、nilは従来Firmの針配置を維持する。明示した新しい強度は形式40へ保存する。筆圧の形式39、Crossの形式38と混在しても、保存とクリップボードでschemaを下げない。

留め縫いは下縫いを含む図案全体の最初・最後へだけ適用する。輪郭と内側、複数runの生成ラッパーにも同じ強度を通し、各内部区間へ勝手に追加しない。針数算定と実生成の一致、全体の上限を維持する。

接続線は既存のStitchPlanBuilderから実針と色ブロックを読み取る。輪郭と内側の糸が違う場合も、元の最後の針で使った糸・メーカー情報を引き継ぐ。previewは文書全体のsnapshotと対象・編集版・入力世代に結び付け、古い結果は適用しない。追加候補の総針数と命令数も計算してから提示する。

最新の2段ワークスペースへ合わせ、編集の関連操作から常設パネルを開く。キャンバスに仮の線を表示し、元・次の図案と糸を確認してから追加する。処理・取消・失敗・追加の主操作はパネル下部の固定領域に置く。

判断したこと・トレードオフ

  • ライブ接続ではなく独立した線にした。参照の循環や再配置時の自動変更を増やさず、既存の編集・保存・出力を使える。
  • 枠中心や図案の外接矩形で接続端点を代用しない。複数runや複数色があるため、実針で確認する。
  • 旧文書の省略強度はそのまま残す。上流や別製品の既定値へ一括変更して、既存の針列を変えない。
  • 実装は機能ごとに別ワークツリーで並行し、統合とローカル受入を親が担当した。独立レビューは同じ1体で仕様適合・品質・統合を確認し、実装担当の自己点検とは区別する。

ハマったこと・学んだこと

  • UIの外側へ最小高さを付けても、buttonStyle内の実際の操作領域は34.5ptのままだった。ラベル側へ最小58ptを付け、AXの実領域を測る試験で確認した。
  • 常設ボタンは計算中も存在する。非同期previewの完了を存在待機で判断せず、適用ボタンが有効になるまで待つ。
  • 古い形式の文書をpreview APIへ直接渡すと、新しい線の糸・名前metadataを旧schemaへ追加して拒否される経路があった。既存EditorSessionと同じ移行済みsnapshotを使う赤・緑試験を追加した。
  • 独立レビューでは、旧strength=nilの最後の端を個別OFFにすると設定全体をnilにして長さまで失う経路が見つかった。新しいPickerだけの試験では検出できない。既存設定を無効状態でも残し、未設定へのfalseは無変更とする境界を試験した。
  • パネル再表示後の仮想化Listでは、画面外の行がAXから消える。対象の位置に合う方向へスクロールし、別の非表示フォームを操作しないよう可視領域に限定した。
  • 最大文字では、AXのScrollViewが割当高ではなく内容の境界を返した。実画像と分類ボタンの90.5ptを使って文字拡大が適用されたことを確認し、操作の到達性を検証した。
  • 出力のバイト比較ではJEFの生成日時が変わった。針の差と誤認せず、14byteの日時以外の全バイトと独立readerの座標・命令を確認した。

4タブへ整理し、設定の場所と貼り付け後の縫い順を揃える

ux未リリース

4タブへ整理し、設定の場所と貼り付け後の縫い順を揃える

リリース状況

未リリース。PR14をmainへ統合済み(merge: 9650f818404e6b9756a413d2dd5a0e740be45f38)。ローカルSimulatorでの実装・検証の開発記録。App Storeへの配布とミシンでの試縫いは行っていない。

何を変えたか

上部を挿入・編集・確認・設定の4タブに整理した。手描きと点入力を挿入へ統合し、2段目は現在値の表示専用にした。横画面では右側を縦スクロールし、縦画面では上部3段目を横スクロールして同じ設定へ到達する。縦画面の上部は画面高の約1/3に収める。

図案の操作は左一覧の各オブジェクトへ置き、複製ボタンを撤去した。貼り付けは選択したオブジェクトの直後へ挿入して次に縫う。グループは途中に割り込まず全体の後ろへ配置し、コピー元の相対順を保つ。選択なしでは末尾へ追加する。

なぜ変えたか

上部の基本操作と右側の詳細設定に同じ項目があり、設定の場所が分散していた。図案操作も左一覧から離れた共通ボタンになっていたため、対象と操作の関係を明確にした。

ユーザーにとって何が良くなったか

素材の選択から作成設定、輪郭・内側の編集と詳細設定を同じ作業面で続けられる。縦画面で設定がキャンバスの大半を覆うことを抑え、コピーした図案を縫う順番も一覧上の位置と一致させる。

技術的な内容

SwiftUIの共通フォームとLayoutで、縦スクロールと高さを限定した横スクロールを切り替える。詳細の展開は同じスクロール面へ項目を追加する。UIHostingControllerでフォームを保持し、回転・タブ変更で入力中の文字列や未適用の値を失わない構成を維持した。

貼り付けはEditorSessionへ挿入基準のIDを渡し、読み込み・変換・検証・追加を単一の履歴操作として確定する。元文書と針データの区別は変えない。

ハマったこと・学んだこと

非表示のUIHostingControllerのviewだけを隠しても、同名の設定がテストのアクセシビリティ階層へ残ることがあった。SwiftUIルートにも表示状態を渡してaccessibilityHiddenを設定し、視覚的な非表示と読み上げ対象を揃えた。

設定の数が増える画面では、上部と詳細の両方へ編集UIを追加するより、現在値の表示と編集場所を分け、同じモデルを参照させる方が重複を管理しやすい。

独立レビューでは、配置を変える過程で生成の取消・失敗表示と糸履歴の入口が抜けたことを検出した。共通の状態表示と色設定内へ復帰させ、再レビューで解決を確認した。糸履歴は、その場で正本へ適用する旧クイック操作と、適用を待つ新フォームで同じメニューを共有し、更新先だけを分けた。履歴選択の時点で上部の現在色が変わらず、適用後に変わることをUIで検証した。

Firebase Storageのmetadata省略を実接続検証で確認

bugfix未リリース

Firebase Storageのmetadata省略を実接続検証で確認

リリース状況

アプリは未リリース。Storageルールの限定修正は利用者の承認後に反映し、既存テストユーザー3名を使うSwiftクライアントの公開・検索・反応・公開取り消しが成功した。ストア配信やGoogle実アカウントの認証を確認済みとは扱わない。

何を変えたか

Storageルールが任意のmetadataを直接参照していた箇所を、未指定なら空mapとして扱う形へ変更した。iOSの公開画像には非機密の固定プレビューマーカーを設定した。メール確認・本人所有・パス・JPEG・サイズ・不正download token指定拒否の条件は維持した。

なぜ変えたか

Emulatorで権限テストが通っても、実StorageへのSwiftアップロードが403になるケースを確認したため。ログイン成功と、画像の公開成功を分けて検証する必要があった。

ユーザーにとって何が良くなったか

ローカルの編集原本を保持したまま、確認済みユーザーが作品の画像と紹介を公開できることを実接続で確認できた。カテゴリとタイトルで探し、別アカウントでいいね・お気に入りを使い、反応解除・非公開まで確認した。

技術的な内容

Rules評価APIへmetadata省略・空map・クライアント指定tokenの3ケースを渡した。旧ルールは省略時だけ未定義参照で失敗し、修正後は許可・許可・拒否という期待をすべて満たした。Firestore参照はmock化し、この評価自体では実データを書き換えていない。

実接続テストはアプリと同じSwiftサービス・SDKを使い、いいねの同一操作の再送でカウントが増えないこと、お気に入りが別人へ混ざらないこと、公開によって原本snapshotが変わらないことも確認する。試験紹介は固有IDを使い、確認後は反応を除去して非公開へ戻す。

ハマったこと・学んだこと

Emulatorは任意metadataを空mapで補完し、予約download tokenを評価前に別の領域へ移す実装だった。同じ入力を実サービスの評価エンジンにも渡すことで、Emulatorだけでは見えなかった差異を検出できた。

反映直後にも403が発生したが、後続の現構成では通し検証が成功した。ルール反映時間と非空マーカーの寄与は分離できていないため、どちらか単独を原因解消の証拠とは扱っていない。

検証メモ

  • Firebase iOS SDK 13.1.1、GoogleSignIn 10.1.0。
  • 最新編集機能を含むCore 993件、App unit 617件(実接続用1件skip)、Functions 22件、Emulator 16件が失敗0。
  • 未リリースのアプリをローカルSimulatorから実バックエンドへ接続した結果。配信済みアプリの本番受入とは区別する。
  • 確認メールのリンク、Googleの実同意・再認証、物理Pencil・試縫いは未確認。

編集の不要機能を減らし、文字サテンと直接編集を追加

ux未リリース

編集の不要機能を減らし、文字サテンと直接編集を追加

リリース状況

未リリース。ローカル実装・検証と独立レビューを終え、PR #20をmainへマージ済み(f75d5aa151bf639a0eee5e3c1bad66f1aa3e22b0)。本番提供・実機試縫いは未確認。

何を変えたか

テーマ配色を専用画面・生成処理ごと削除した。通常の糸色変更・登録糸は残した。認証画面はログイン/新規登録を人物チェック/人物追加のアイコンと文字で選ぶ。図案の名前は一覧のタイトル位置から直接変更し、別の入力欄を下へ重ねない。編集設定は初期折りたたみ、計測単位はものさしを除いて文字だけで選べるようにした。重複した右上レイヤーボタンを取り除き、一覧が閉じている時だけキャンバスのヘッダーに復帰口を出す。

現在値行の開閉ボタンは1段目に固定した。閉じる際に行全体を取り除き、制作領域を広げる。文字の内側をサテンで縫う設定も追加した。

なぜ変えたか

利用者が実機のスクリーンショットへ記した指摘を元に、必要ない機能、重複する入口、名前を変える場所のずれ、折りたたんでも領域が広がらない操作を整理した。

技術的な内容・学んだこと

開閉後も44ptのボタン行を残すと、48ptの現在値を閉じても差は4ptしかない。ボタンを消したい行の外へ置き、行全体を条件表示にする必要がある。状態を変えたことと、利用者の作業領域が実際に広がることは別の受け入れ条件として扱った。

図案名の途中入力はオブジェクトIDをキーにモデル側で保持する。横の左一覧と縦の上部一覧が別のViewになっても入力を失わない。確定は既存の文書編集とUndoへ接続する。装飾アイコンはVoiceOverで重複して読み上げず、日本語ラベルと選択状態を付ける。

文字サテンはフォント由来の編集可能な輪郭・穴を維持し、一定角度で対岸へ渡る針を生成する。穴・外側を横切る接続は既存の有界な接続判定で拒否しジャンプにする。針数見積もりと生成は同じ経路を使い、最大針目長・仕事量上限・取消を維持する。新しい保存フラグを旧アプリが黙って無視しないよう文書schema41に区別し、旧schemaへの保存を拒否する。

独立レビューで見つかった2つの境界

対岸への接続距離と、1針の最大長は別の判定だった。幅が最大針目長より太い文字でも、全体の接続が字形の内側なら、接続自体をjumpにせず短い針へ分割する。幅6mm・最大3mmの回帰で、修正前の20runから単一runへ変わり、全needle距離が3mm以下で穴・文字間のjumpも保持されることを確認した。

キーボードを出すとSwiftUIの作業領域の高さが短くなるため、contentの幅>高さを画面回転の判定に使うと、portraitのまま右ドックへ切り替わってしまう。編集画面が属するUIView.window.boundsを読み、各ウィンドウの実寸で配置を決めるようにした。別Sceneやグローバルな画面寸法を参照しない。未確定の「-」を設定の閉再開と回転をまたいで保持し、キーボードを開き直して訂正・適用する操作まで検証した。

判断と制約

文字の筆画に沿って方向が自動変化するデジタイズではない。角度と間隔を利用者が調整し、長い針は最大針目長で分割する。Pencil実機・試縫い・ミシン互換はSimulator成功で代替しない。未リリースのためアプリ内のリリース告知は追加しない。

検証

Core全988件、アプリ単体全634件(実Firebase接続1件skip・失敗0)が成功。専用iPad Simulatorでは認証の切替、文字サテンの保存再開、名前編集・単位選択・回転、削除とUndo、未確定数値の閉再開・回転・訂正適用を確認した。独立レビューの2指摘を修正し、2ef0e2aで再レビュー承認(未解決の必須・任意指摘0)。PR #20のhead・base・差分一致とレビュー記録投稿を確認してmainへマージした。GitHub上のCIチェックは設定なし。実アカウントの登録・削除・確認状態の変更は行わず、認証フォームは送信せずに切替だけ確認した。

ガイド・確定・色・オブジェクト操作を目的に合う場所へ戻す

ux未リリース

ガイド・確定・色・オブジェクト操作を目的に合う場所へ戻す

リリース状況

未リリース。PR17をmainへマージ済み(a4da2b526211d0b5e4ecd0b58220d45fff2d02ca)。ローカルiPadOS Simulatorで確認した開発変更。配布や試縫いの完了とは区別する。

何を変えたか

使い方ガイドを設定欄の内部から画面全体のオーバーレイへ戻す。背景の暗幕、説明箇所の枠、案内カードを重ね、戻る・次へ・スキップ・完了と設定からの再表示を維持する。

描いた線・新規マス図案・手動針の確定はキャンバス内右下へ固定する。オブジェクトの名前変更は左の図案・縫い順へ移し、削除はメニューから外して対象行のゴミ箱を直接表示する。コピー・貼付の順序とUndoは維持する。

自由な糸色はRGB3本のバーから、色相環・中心からの鮮やかさ・明るさへ変更する。6色の見本へ最近の使用履歴を統合し、最後に使った異なる色が左へ来る。未使用時と足りない枠は従来の基本色を保つ。独立した最近糸のメニューを除く。

操作ボタンは枠と背景、文字入力はAaと入力枠、数値入力は123と単位・数値キーボードで表示ラベルと区別する。基本の設定見出しも開閉できるようにし、入力を破棄せずに折りたたむ。DESIGN.mdを同じ規則へ更新する。

なぜ変えたか

設定を同じ面に集約するだけでは、押す・入力する・読むという操作の違いが分かりにくかった。ガイドや描画の確定まで設定スクロールへ入れると、説明対象やキャンバスの操作と距離ができる。利用者の作業対象に近い配置と、操作の種類が分かる共通表現を優先した。

ユーザーにとって何が良くなったか

確定や削除を探すために設定やメニューを開く手間が減る。色は数値の組合せを考えずに円から選べる。色の見本と履歴が一つになり、使った色へ戻しやすい。必要な設定だけを開き、入力途中の値を保ったまま画面を整理できる。

技術的な内容

確定は既存モデルの検証・非同期処理・Undoへ接続した共通ViewをCanvasのoverlayへ置く。表示のためにCanvas自体の寸法を変えない。ガイドの対象枠はAnchorを画面座標へ解決し、回転に追従する。

色相環はAngularGradientと中心からの白いRadialGradientで色相と鮮やかさを示す。黒から色を選んだときも選択を目視できるようにし、明るさ0でも保持した色相・鮮やかさを再調整できる。既存のHEX・登録糸・草稿Bindingへ接続する。

マス描画の草稿には実際に使った色の順を保持し、草稿Undo/Redoのグリッドと同じsnapshotへ保存する。確定時のpalette順や最後に選択したbrushだけから履歴を推測しない。

最近色は成功した確定だけで記録し、取消・失敗・Undo/Redoや自動解析の色を使用履歴として扱わない。同色の登録糸が多数あると他の色が保存上限から追い出されるため、保存側でも表示HEXを基準に重複を除き、最後の登録情報を保持する。既存version1の設定は読み込める。

ハマったこと・学んだこと

カスタムTextFieldStyleを入れ子のフォームへ継承すると、configurationへ同じ装飾が再適用され、枠や入力記号が何重にもなって入力できなくなった。configurationをplainにしてから外観を付け、装飾はhit testから外す。

SwiftUIのGroupを平坦なLayoutへ渡しながら開閉すると、外側の表示属性で内側の非表示属性を上書きしやすい。実際のSwiftUI Layoutコンテナで階層を分け、子Viewを保持したまま非表示・無効化へ切り替える。開閉部分にUIHostingControllerを増やす方式は、内部から外のスクロールへドラッグが渡らなくなったため撤回した。元からのフォーム切替用ホストは維持する。

展開中の本文を明示的な読み上げグループにすると、非表示子を含む巨大なAX領域になり、他のボタンの自動操作判定へ重なった。不要なcontainを除き、閉鎖時はscaleEffect(0)で子の領域も縮め、子の標準アクセシビリティを使って通常のButton.tapで再確認した。

XCTestには非表示UIKit入力の以前のスナップショットが残る場合があった。existsだけで開閉を判定せず、閉鎖状態・無効化・実画面・再展開後の文字列保持を合わせて確認する。これだけで実機VoiceOverの操作を確認したことにはしない。

自動スクロールを数値入力やトグルの上から始めると、ドラッグが別操作へ取られる場合があった。共通ScrollViewは背景と矩形のcontentShapeを持たせ、内容の余白からもスクロールできる操作面として扱う。テストも余白からドラッグし、近い対象は中央へゆっくり寄せ、遠い対象は速く移動する。キーボードで列が再配置された後も、入力欄を画面内へ戻してから編集する。

独立レビューでは、通知がガイドの背景抑止の外にあることと、未選択グループのゴミ箱の表示対象が実削除より狭いことも見つかった。ガイドoverlayは通知を含むルートへ移し、通知の操作・読み上げを抑止する。行の削除は、選択と同じグループ展開を件数説明・メンバーやレイヤーのロック判定・実処理に使う。

設定の所属整理、作品管理、カテゴリ拡充

feature未リリース

設定の所属整理、作品管理、カテゴリ拡充

リリース状況

未リリース。iPadアプリの作業ブランチで実装。新カテゴリのサーバー許可定義を更新したが、Functionsの本番デプロイは行っていない。実機Pencilと試縫いの確認は未実施。

何を変えたか

挿入・編集・確認・設定の4タブの下に、目的別の開閉できる中項目を置いた。色・余白・ボタン等の既存デザインは保持し、設定の所属と閉じる範囲を整理した。「自由な色」の中へ色番号入力・適用・エラーも含め、見出しを閉じても関連操作だけが残る状態をなくした。

作品カードの長押しと詳細から、このiPadの作品の名前変更・削除を行えるようにした。原本の削除と公開紹介の取り下げを区別して説明する。既定カテゴリは既存5種類の識別子を維持し、40種類へ増やした。

なぜ変えたか

実機スクリーンショットに、折りたたんでも色番号入力が残ること、作品の名前変更・削除ができないことが指摘されていた。追加設定を画面へ継ぎ足すと、同じ対象の操作が挿入と編集に混在しやすいため、全小項目に目的と所属を定めた。

技術的な内容

SwiftUIの既存WorkspaceSectionとWorkspaceDisclosureGroupを再利用し、閉じた内容の状態保持とタップ・読み上げ除外を継続する。文字・曲線・下絵について作成と既存対象の編集を分ける。生成処理の取消は設定のスクロールや開閉に依存しない位置へ接続する。

改名はパッケージの移動で実装し、文書IDや編集正本を書き直さない。同名の移動先があれば番号を付ける。カテゴリ台帳はIDをキーにするため、名前変更で分類を失わない。削除は独立した退避コピーを作ってから原本と台帳を更新し、部分削除後の失敗でも完全なコピーから復元する。

カテゴリ追加はSwiftのenumだけでなく、公開入力を検証するFunctionsの許可集合も更新する。未知のカテゴリとObjectのプロトタイプ名は引き続き拒否する。

ハマったこと・学んだこと

  • 見た目の囲みだけでは設定の所属は伝わらない。関連入力と適用ボタン、エラーを同じ開閉単位に含める必要がある。
  • 外側にアコーディオンを追加すると、既存の子フォームにあった取消まで隠れる。開閉範囲のレビューには処理中の導線も含める。
  • 同じ部品の再利用でも、作成タブと編集タブへ同じ対象の更新操作を出すと重複になる。作成/編集の文脈を明示する。
  • 保存済みカテゴリは識別子互換が重要。表示名や件数の変更と、サーバーの許可集合の更新を同時に確認する。

検証と今後

アプリのSimulatorユニットテストは638件成功・実接続用1件skip。Functionsのドメインテスト10件が成功。改名のID・内容・カテゴリ保持、同名衝突、外部作品拒否、削除対象限定、壊れた分類台帳の保持を確認した。UIの開閉・回転・途中入力・作品管理の検証結果はアプリのQA文書へ記録する。

本番反映後の公開操作、Pencil実機操作、ミシンでの試縫いは別途確認する。この記事材料をリリース告知として扱わない。

2段リボンと常設パネルで制作の場所を整理する

ux未リリース

2段リボンと常設パネルで制作の場所を整理する

リリース状況

未リリース。iPadネイティブ画面の改善記録であり、App Storeでの提供、物理Pencil、試縫いの成功を意味しない。実行結果と未確認範囲はアプリ側の docs/design/qa/2026-10-28-two-row-workspace/QA.md で管理する。

何を変えたか

上部1段目で作業目的、2段目で挿入する素材・手描き/点入力・線の見た目を選び、詳細は右側の常設パネルで調整する。空き領域には幅・間隔・下縫い等の現在値を表示する。

ホームは編集設定の再掲から、作品の状況と制作再開、素材追加、仕上げまでのガイドへ変更した。縫い順と図案のコピー・貼付は左側へ集約した。上部2段目と左右パネルを独立して閉じられ、再表示して入力を続けられるようにした。

設定、準備、保存情報、書き出し前確認、ガイド、色指定、書体・ライセンス、復旧は画面内の作業面で扱う。OS標準のファイル選択・書き出し先選択は維持する。描画確定は右下に固定し、対象を明記した大きなボタンにした。縫い方には線・横振り・行埋めの模式図を添えた。

なぜ変えたか

ホームが編集・描画と同じ設定を表示し、作業の入口として役割を持っていなかった。素材を入れる目的の選択と、素材の種類や数値の選択が混ざり、モーダルの開閉でキャンバスを見失いやすかった。先に横画面HTMLで操作構造を確認し、ユーザーが承認した構造をネイティブへ移した。

ユーザーにとって何が良くなったか

「何をするか」「何を使うか」「細かくどう設定するか」を別の場所で判断できる。キャンバスを広げたいときは必要なパネルだけを閉じ、開き直しても入力を続けられる。縫い方は専門名を知らなくても模式図で比較できる。

技術的な内容

正本・針生成・Undo・文書codecを維持し、画面の配置と表示状態を分離した。SwiftUIのLayoutで左一覧・キャンバス・詳細の同じ子Viewを配置し、回転時には横ドックと上部の作業面を切り替える。非表示時も入力フォームを保持し、表示・ヒットテスト・アクセシビリティを制御する。

草稿は確定済みの図案と区別し、生成中の取消、失敗時の入力保持、古い結果の拒否は既存のモデルを利用する。開閉は正本の変更とせず、Undo履歴を増やさない。

Before / After

以前は横画面の基本設定も右側に集まり、詳細フォームが中央モーダルだった。変更後は2段目に操作の選択と現在値、右に詳細、左に図案管理を分けた。準備や確認も独自のモーダルを介さず操作する。

判断したこと・トレードオフ

設定フォームの作り直しではなく、既存の入力状態を保持する配置変更を選んだ。専門的な設定を削除せず、詳細の作業面へ残す。操作場所の整理は針生成や実際の縫い上がりを改善・保証するものではないため、表示と縫製品質の検証は分離する。

ハマったこと・学んだこと

シートをパネルへ移すだけでは、親ホストがnavigation barを非表示にする設定を引き継ぎ、内側のNavigationStackの操作まで隠れる場合があった。Simulatorで準備ボタンの不在を検出し、画面と階層を照合して、パネル自身の見出し・スキップ・固定開始ボタンへ置き換えた。

保持するフォームの存在と、利用者が操作できるフォームは同じではない。テストは単に存在を探すだけでなく、表示範囲とヒットテスト可能な操作を確認する必要があった。初回ガイドのように入力草稿を持たないビューは、必要なときだけ表示する構造へ整理した。

Googleログインとメール確認による会員機能の制限

security未リリース

Googleログインとメール確認による会員機能の制限

リリース状況

未リリース。Swiftビルドと関連unitテスト41件、Node 22件、Firestore/Storage Emulator 16件に成功。本番の認証設定・デプロイ・確認メール実送信は未実施。

何を変えたか

Apple認証を外し、Googleとメール・パスワードを採用した。メール登録時に確認メールを送り、確認前は作品公開・いいね・お気に入り・通報を拒否する。確認待ち画面に再送、確認状態の再読込、ログアウト、本人確認して退会を追加した。

なぜ変えたか

初期段階の認証方式を絞り、メールの所有確認を会員機能の条件とする要望があったため。

ユーザーにとって何が良くなったか

Googleアカウントでログインできる。メール登録者は次に行う操作を画面上で確認できる。未ログイン・未確認でも自分の作品をオフラインで制作・保存・再開でき、編集用原本は端末に残る。

技術的な内容

GoogleSignIn iOS 10.1.0とFirebase iOS 13.1.1を使用。GoogleコールバックURLと文書を開くURLを分岐する。権限判定はキャッシュされたUser状態だけに頼らず、強制更新したIDトークンの本人UID、provider、email_verifiedを検証する。

同じ制限をCallableとFirestore/Storage Rulesにも適用する。許可providerはpasswordとgoogle.com。古い未確認トークンを拒否し、未確認本人の退会は別の認証条件で許可する。

ハマったこと・学んだこと

認証状態リスナーと明示的な再読込が重なるため、UIDと処理世代を照合して古い非同期結果を破棄する必要があった。Google再認証では別アカウントへ切り替えず、現在の本人と一致することを検証した。

npmのENOENTは依存不足ではなく、機能が未マージのmainでコマンドを実行していたことが原因だった。デプロイ手順には実装ワークツリーへ移動するcdを含めた。

ローカルの制作原本とFirebaseの公開作品を分けるアトリエ

feature未リリース

ローカルの制作原本とFirebaseの公開作品を分けるアトリエ

リリース状況

未リリース。iPadアプリへの実装作業とローカル検証の記録。Firebase本番へのデプロイ、Apple認証の本番接続、App Store配信、Pencil実機や試縫いの確認を完了した意味ではない。アプリ側の docs/design/qa/2026-10-28-firebase-atelier/QA.md と docs/firebase/VERIFICATION.md に到達点と未確認事項を記録する。

何を変えたか

  • ホームを「自分のアトリエ」「みんなのアトリエ」の2つの下部タブへ変更する。
  • 作品原本の作成・保存・再開はローカルのままとし、制作にログインやネットワークを要求しない。
  • メール/パスワードとApple認証、作品紹介の公開・取り消し、いいね、お気に入り、カテゴリ別・新着/人気順・前方一致検索を追加する。
  • 公開対象はJPEGプレビューと紹介情報。編集可能な.stitch原本とミシン用ファイルは送信しない。
  • Firebase未設定時や接続停止時も制作経路を維持する。

なぜ変えたか

作品づくりの継続と、他の人の作品から着想を得る行動を両立したかった。ネットワークやアカウントの状態に原本の作成・保存を依存させると、オフラインのiPadで制作できなくなるため、公開側だけをオンラインの責務にした。

ユーザーにとって何が良くなったか

自分の作品を棚から再開し、見せたいものだけを公開できる。公開を取り消してもローカルの原本は残る。いいねは作者に向けた反応、お気に入りは本人だけのコレクションとして分けた。ローカルカテゴリは作品とは別の台帳へ保持し、分類操作で図案の形状・針設定を書き換えない。

技術的な内容

アプリはSwiftUI、Firebase Authentication、Firestore StandardのCoreクエリ、Cloud Storage、Callable Functions v2を使用する。既存のUIDocument編集セッションと.stitch codecを引き続き使う。サーバー側はJavaScriptで実装し、Firestoreのクライアント直接書き込みを拒否して、認証・入力・所有者を検証するFunctionsを唯一の書き込み入口にする。

投稿IDを利用者IDとローカル文書IDから決め、公開取り消しでは投稿をunpublishedへ変更する。削除して同じIDを作り直すと、残っていたいいねと表示件数がずれるため、再公開でも作成日時と件数を保持する。反応は本人の追跡文書と投稿件数を同じトランザクションで更新し、同じ操作の再送を冪等にする。

画像はストレージのパスとして保持し、トークン付きのダウンロードURLをアプリが保存しない。Storageルールは公開状態と現在の画像パスを照合する。Firebaseのトークン自動生成も考慮し、公開取り消し・画像差し替え時にはトークンを変更して以前のURLを失効させる。ただし、すでに取得された画像自体を回収できる意味ではない。

Before / After

Before

標準文書ブラウザから制作を開始し、公開作品やアカウントの機能はなかった。

After

作品棚と公開ギャラリーをホームへまとめ、ローカル制作とオンラインの公開・反応を分ける。原本・復旧下書き・ミシン用出力の区別は維持する。

判断したこと・トレードオフ

  • 原本の同期・バックアップや他人の編集データ取得は今回の公開機能に含めない。
  • カテゴリ絞り込みと正規化した前方一致検索を実装し、日本語の任意部分一致や全文検索を実装済みとは扱わない。
  • 画像アップロード後に公開の応答を失うケースでは、クライアントがすぐ画像を削除しない。公開自体は成功している可能性があるため。公開されず残った画像の定期清掃は今後の運用課題として残る。

ハマったこと・学んだこと

JavaScriptのObject.hasOwnはキーを文字列へ変換する。カテゴリの存在確認だけだと、文字列の代わりに配列で送った値も受理され、Swiftのenumデコードを壊すことがあった。カテゴリを文字列型として検証し、各フィールドの配列・オブジェクト・数値文字列・未知フィールドを拒否する回帰テストを追加した。

検索語の正規化はサーバーとクライアントを一致させる必要がある。NFKCと小文字化だけでなく、連続空白・改行・全角空白も単一化し、「Blue Bird」を「Blue Bird」で探せるようにする。

Swift 6ではSDKへ渡す辞書の非Sendable境界も型検査の対象となる。呼び出しごとに新しい入力を作り、再利用しない値として所有権を渡す。アプリ全体の並行性チェックを弱めて解消したことにはしない。

Pencil筆圧と装飾サテンを編集可能な中心線へ接続する

feature開発中(未リリース)

Pencil筆圧と装飾サテンを編集可能な中心線へ接続する

リリース状況

コードはPR12でmainへ統合済み(merge eb2ad9a01f0207c1ccba1be5aa9b3bbfa3bbed71)。アプリは未リリース。実装とローカル検証の開発記録。App Store配布や物理Pencilの追従、ミシンでの試縫い成功は確認していない。最新の到達点はアプリ側の docs/design/qa/2026-10-28-expressive-satin/QA.md で管理する。

何を変えたか

固定幅のサテンに、記録したPencil筆圧による幅変更、小区間を3回縫うBean zigzag、左/右/両側の羽毛状の縁を追加した。中心線と入力圧、生成設定を別に保持し、形式39の保存・コピー・変形・Undo・既存出力へ接続している。

なぜ変えたか

一定幅の線だけでは、Pencilで描く強弱を刺しゅうの帯へ反映できなかった。生成済みの針へ直ちに平坦化すると、最小幅や装飾を後から調整するための情報も失われる。中心線と入力記録を正本として残す方式を選んだ。

ユーザーにとって何が良くなったか

最小/最大幅を決め、筆圧と縁、往復の強調を組み合わせて再編集できる。指・点入力や有効な筆圧記録がない入力は一定幅と明示する。途中の数値は対象別に残し、不正値があれば最後の有効値を無言に適用しない。

技術的な内容

圧力サンプルは中心線の正規化弧長位置、raw force、maximumPossibleForce、推定値の有無を持つ。force == 1 を最大圧と仮定せず、入力機器の上限で正規化する。PencilKitの曲線を保存し、UIKitのcoalesced touchから取得した圧力を対応させる。入力の世代と表示領域が変わった一筆は採用しない。

非等方拡縮では、全体の弧長比率をそのまま残すと曲がりの前後で圧力位置がずれる。元の辺と変形後の辺の長さから位置を写し、raw forceと機器上限は変えない。輪郭の方向反転ではサンプル順序と位置を反転する。圧力を持つ閉線は始点を維持して方向を変え、同じ物理位置の幅が周回方向へずれないようにする。

羽毛は保存したseedと断面番号から決定的に計算し、指定幅の内側へ変化させる。Beanは長い針を先に小区間へ分け、各小区間をout/back/outにする。帯全体を3回繰り返す方式にはしない。針数の事前計算と実生成を一致させ、針数・処理量・取消の上限を維持する。

新設定がなければ旧生成器の同じ経路を使う。帯プレビューは生成と同じ断面を非同期に計算するが、描画用にも独立した頂点上限を持ち、表示できないときは編集元の中心線を残す。

判断したこと・トレードオフ

  • raw入力と生成応答を分け、設定コピーで別の線の圧力記録を上書きしない。
  • 型の省略可能フィールドと形式39のゲートを使い、古い文書の固定幅を変えない。
  • UIKitの推定値を記録に残し、すべてが確定した測定値だとは表示しない。
  • 全弧長の筆圧記録を切断・結合後へそのままコピーすると幅が移動する。従来の切断・結合は圧力記録を持つ場合に拒否して正本を保持し、圧力対応の位置写像を後続項目として追跡する。
  • 筆圧機能のない入力をアプリの描画失敗にせず、記録なしの一定幅として扱う。
  • 並行でmainへ入った折りたためる2行ワークスペースを統合し、設定を常設パネル、現在値を同じモデルから読む2段目へ配置する。古いモーダル配置を復活させない。

ハマったこと・学んだこと

  • schema39とCrossの形式38が混在すると、旧昇格コードが39から38へ下げる危険がある。形式の比較を最大値で扱い、保存とクリップボードの両方を試験した。
  • 一筆の草稿履歴には圧力も含める。スタイルだけ戻したときに圧力を消したり、次の一筆へ前の記録を流用したりしない。
  • 未適用数値と、既にプレビューへ反映した数値を区別する。反映済みの文字列が新しいクイック変更を巻き戻さず、不正な途中入力は保持する。
  • 下縫いにも輪郭のずらしを反映する必要があった。装飾を外した共通断面から下縫いを計算し、上縫い間隔を変えても下縫いは変わらない試験を加えた。
  • 閉線の反転はサンプル順序を逆にするだけでは足りなかった。輪郭の始点が旧末尾へ移ると弧長の原点も変わる。非対称な圧力と同じ物理位置の実レール幅を試験し、独立レビューの指摘を修正した。非直線Bezierの拡縮も、独立した多項式参照と中間圧力で補強した。
  • OS文書一覧の遷移中は同じ識別子の候補が複数ある。表示の安定を待ち、常設パネルの可視行を取り直すUIテストにした。Simulatorのアニメーション通知待機で中断した一括試行は成功に含めず、同じ判定の独立実行を記録した。
  • XcodeGenの通常グループへ新しい試験ファイルを追加した後は再生成が必要。対象0件のTEST SUCCEEDEDを、新試験の成功として扱わない。

編集正本を保った再利用と糸選び・針位置の確認

feature未リリース

編集正本を保った再利用と糸選び・針位置の確認

リリース状況

未リリース。iPadネイティブアプリでの開発記録であり、App Store配布や実機Pencil・試縫いの合格を意味しない。ローカルの実行結果と未確認範囲はアプリ側の docs/design/qa/2026-10-28-editing-efficiency/QA.md を正本とする。

何を変えたか

  • 図案のコピー・切り取り・貼り付けを追加し、作品間でも編集可能な形状・文字・針・画像方式と共有元PNGを運べるようにした。
  • 通常の線や面の縫い設定を別の図案へ適用する操作を追加した。形状や穴は適用先のものを残す。
  • 確定した糸・色の直近12件をアプリ共通で保持し、メーカーや糸番号を含めて再利用する。
  • 選択部分の数値設定だけを初期値へ戻し、方式・糸・穴・形状を維持する操作を追加した。
  • 図案一覧へ方式、メーカーと糸番号、図案ごとの生成針数を表示した。
  • 針落ち点に自動・表示・非表示を設け、大量の針でも画面内の実際の位置を確認できるようにした。

なぜ変えたか

未実装一覧を優先順位付けし、日常の制作・再利用に直接役立ち、既存正本と検証経路を使える機能を先行した。新方式の生成品質やメーカー通信を一度に広げるより、選択した図案の再利用と編集の往復を減らすことを優先した。

ユーザーにとって何が良くなったか

同じ作品内の複製だけでなく、別作品へ図案を持ち込める。縫い設定をそろえるときは形を変えずに再利用し、糸選びは実使用履歴から続けられる。設定を調整しすぎても形や糸を消さずに数値だけ戻せる。一覧と針落ち点で、選択前後の内容を把握しやすくした。

技術的な内容

クリップボードは独自のバージョン付きデータに既存文書codecを組み合わせ、20MiBの上限と正本の検証を再利用する。貼り付け時は図案・グループ・共有画像参照のIDを再発行し、複数画像方式が同じ元PNGを使う関係を保つ。貼付先の作品設定は変更せず、選択中のレイヤーへ相対位置を保って追加する。

切り取りでは先にコピー内容と削除候補を別セッションで検証し、OSクリップボードの書込を確認した後だけ削除を採用する。コピー失敗で元図案を消さず、貼り付けと切り取りはそれぞれ単一Undoへ接続する。非同期結果は文書・編集版・選択・入力世代・レイヤーを照合する。

履歴は作品の保存形式ではなくアプリ設定に分離する。成功した編集前後の糸割当だけを比較し、移動・選択・未適用入力やUndo/Redoを使用履歴として数えない。Photo/Paint/Sketchの専用糸割当は今回の履歴対象外で、解析パレットの初期色を実使用として記録しない。

図案ごとの針数は生成済み計画の .stitch だけを図案ID別に集計する。ジャンプや糸切りを針数に含めず、生成中・失敗・未生成・非表示を0針と区別する。点表示は画面外を除き、512点ずつ描画用Pathへまとめ、明示表示のときに針数だけで全点を隠さない。

Before / After

Before

作品間で編集正本を再利用する入口がなく、縫い設定の再入力や糸色の再選択が必要だった。針落ち点は少数針での自動表示だけで、一覧は名前と順序を中心に表示していた。

After

図案全体と設定だけの再利用を分け、どちらも正本とUndoを保つ。実使用履歴と、正本・最新生成結果に基づく一覧を提供し、点表示を利用者が選べる。

判断したこと・トレードオフ

  • 図案クリップボードと縫い設定コピーは異なる操作にした。穴は設定ではなく形状情報なので、設定コピーで持ち込まない。
  • 作品全体の糸ライブラリを無条件でコピーせず、選択図案が実際に参照する糸だけを運ぶ。
  • メーカーだけ入力されて糸番号がない登録糸は、名前やHEXを補い、同メーカーの別色を区別する。
  • 履歴は12件に限定し、破損した設定は安全に空から再開する。履歴消去で作品のUndo履歴を増やさない。

ハマったこと・学んだこと

初期の図案コピーでは元作品の登録糸をすべて転送していたため、未使用糸だけで貼付先の件数上限に達するケースがレビューで見つかった。登録糸1000件の元から未割当の線をコピーする回帰テストを追加し、選択正本が参照する糸だけへ限定した。

新規空作品の既定の描画状態と、未確定の入力を持つ描画状態は区別が必要だった。何も描いていない作品まで貼付を拒否すると、作品間再利用の入口を妨げる。初期状態を手で書き換えないテストと通常UIの作品間E2Eで確認する。

クロス図案の直接描画とPixel・QRを編集正本へ接続する

feature開発中(未リリース)

クロス図案の直接描画とPixel・QRを編集正本へ接続する

リリース状況

未リリース。実装とローカル検証を進めている開発記録で、App Store配布や物理Pencil・ミシンの試縫い成功を示すものではない。最新の受入結果はアプリ側の docs/design/qa/2026-10-28-cross-pixel-qr/QA.md を正本とする。

何を変えたか

画像変換後の単一マス編集に加え、空のグリッドを作り、共通キャンバス上をPencilや指で連続して塗る入力を追加した。Crossの対角線2本に加えて、セル内を往復するPixelの針を生成する。URLからQRのマスを作り、同じ編集可能な図案として保存・変形・コピー・出力する。

なぜ変えたか

クロスを一マスずつフォームで指定するだけでは、下絵を見ながら自由に描く制作が難しかった。画像方式と同じマスの正本を使えば、直接描画も生成も同じ保存・Undo・糸割当へ接続できる。

ユーザーにとって何が良くなったか

マスをなぞって塗り、色を変え、消しゴムで消し、CrossとPixelを選べる。一筆を取り消せる。描画中に回転や表示倍率が変わった場合は、途中の入力を誤った場所へ確定しない。QRも画像の貼付だけにせず、マスを再編集できる。

技術的な内容

セルの座標は2本の基底ベクトルで保持する。入力点を逆変換してグリッド座標へ戻し、線分とセル境界の交点を列挙する。入力速度に応じたサンプル数に依存せず、最大256行・256列に比例する仕事量で通過マスを求める。

一筆のプレビューと確定済み文書を分ける。既存図案の更新は候補セッションで生成針を検証してから採用し、一度のUndoへまとめる。入力世代・表示領域・編集版・選択対象を照合し、遅延結果を拒否する。新規の空グリッドは草稿として扱い、空のまま編集文書やミシン出力へ紛れ込ませない。

Pixelの行間隔と16色を超えるパレットは形式38の対象にする。従来Crossには省略可能フィールドを使い、旧文書の対角線生成を変えない。クリップボード片も内容に応じた形式に更新する。

QRはCore Imageで生成し、余白を持つ黒いPixelへ変換する。論理マスを独立したVisionのバーコード検出へ渡し、2種類のURLを画像として読めることを確認した。縫製後の読み取りは、糸・生地・密度の影響を含む別の実機受入である。

通常UIから保存したPESも独立したpyembroidery 1.5.1で読み、保存文書から作った生成計画と全2692針の座標、黒1色、停止0件が一致した。DST/EXP/JEFは同じ文書からCLI生成した出力の全座標を照合した。通常Files保存を操作した形式と、CLI生成で照合した形式を区別している。

ハマったこと・学んだこと

  • 草稿の方式・間隔と描画を別々に更新すると、描画全体のUndoスナップショットが新しい設定を巻き戻す。独立レビューで発見し、方式と適用済み間隔も共通履歴にまとめ、変更後の古いRedoを破棄する回帰テストを追加した。
  • Pixelを追加しても、旧Crossの「1マス4針」という統計計算が残ると、表示針数と実出力が食い違う。密度に応じた針数の事前計算を生成と共有し、上限超過を配列確保前に拒否した。
  • 元文書が形式38でも、クリップボード片を既定の37で作ると新設定の検証に失敗する。文書の通常保存だけでなく、部分データを組み立てる境界も確認する必要がある。
  • iPadOS27 SimulatorのVisionでは新しい推論コンテキストを作れなかった。検出リビジョンを明示し、利用可能なバーコードエンジンで独立読取を実行した。例外を成功扱いする代替処理は入れていない。
  • 横スクロールするリボンのテストでは、画面中央をスワイプしてもキャンバスへ入力されるだけだった。実画面の操作領域とAXの型を確認し、リボンの中をスクロールする必要があった。

制作領域を広げ、交差した一筆の輪を面として縫う

improvement未リリース

制作領域を広げ、交差した一筆の輪を面として縫う

リリース状況

未リリース。実装し、Simulatorで配置・編集・保存・出力を確認した。今回の変更は実機へ配布しない。App Store公開・物理Pencil・試縫いの確認と区別する。

何を変えたか

ホーム・挿入・描く・編集・配置・枠・確認の分類を残し、作品名と戻る操作をコンパクトな同じ行へ置いた。独立した共通操作行を廃止し、横画面は左に図案一覧、右に道具と縫い設定を置く。縦画面では道具・色・幅・間隔・方向・確定を上部で操作する。取り消す・やり直すは横画面のキャンバス端、縦画面の上部。作品設定は配置・枠、出力は確認へ移した。

同じ一筆の線が交差して囲んだ面は、内側の面埋めを選べる。始点と終点が離れていても、交差した輪だけを面として縫う。輪の外の尾や元の曲線は消さない。内側の縫いを自動で有効にしない。

なぜ変えたか

大きな作品名・共通操作・分類・設定の各行がキャンバスの高さを消費していた。また、上部が新規草稿、右が以前の選択図案を示すと、設定対象が分かりづらかった。面埋めは「両端が閉じている」という条件だけで判定しており、手描きで交差した輪を拒否していた。

ユーザーにとって何が良くなったか

横向きではキャンバスの上を小さくし、右で道具と縫い方を続けて変更できる。縦向きでは操作を上下に分散させない。手描きの輪を面として使うために、利用者が端点をぴったり合わせる必要が減る。

技術的な内容

  • 同じ操作部品を、横では右、縦では上へ配置する。選択状態や縫い方の値をレイアウトごとに複製しない。
  • 左右パネルの表示判定は幅だけでなく縦横比も含める。大きいiPadの縦画面を横用レイアウトと誤判定しない。
  • 編集可能なBezier曲線と、縫い生成用の派生面を分離する。交点で線分を分け、平面グラフの有向辺を角度順に巡回して有界面を抽出する。開線に終点から始点への辺は追加しない。
  • 尾の往復と重複辺を除き、接触した複数の輪を面へ分ける。入れ子の輪は二重に縫わない。既存の穴は対応する面へ割り当てる。
  • 交点探索・派生点数・巡回・包含判定の仕事量を制限し、取消を確認する。別々の図案の重なりを自動で結合しない。

ハマったこと・学んだこと

開いた曲線を単に閉じると、長い尾と輪の間に意図しない面ができる。「入力の閉鎖状態」と「囲まれた面の存在」を別々に扱う必要がある。曲線の正本をポリラインへ置き換えず、派生面だけを縫い生成へ渡すことで、編集・保存・変形と縫い生成を分けられる。

プレビューも生成用の派生面を使う。開線全体をCanvasで塗ると描画APIが暗黙に両端を閉じ、縫わない部分まで灰色にするためだ。解析は取消可能な背景処理とし、針の再生フレームごとには実行しない。

SwiftUIの配置変更では、同じ右パネルでもアクセシビリティツリー上の種別がコンテナからScrollViewに変わる場合があった。UIテストは安定した識別子で対象を取得し、実際の位置と操作可能性も確認する。

ドラッグ移動と縫い順の操作を整理する

bugfix未リリース

ドラッグ移動と縫い順の操作を整理する

リリース状況

未リリース。ローカル実装・Simulator検証を完了。今回の変更は実機iPadへ配布しない。App Store公開や物理Pencilでの操作を確認済みとは扱わない。

何を変えたか

キャンバスの角ハンドルを廃止し、1本指ドラッグは移動に統一した。選択枠内の空白からも移動でき、サイズ変更は選択上の2本指ピンチと数値入力を使う。左一覧に縫い順の上下ボタンと行ドラッグを接続し、上部の方式・色・幅と右側の間隔・補正・下縫いを分けた。

なぜ変えたか

小さい図案では角の判定領域が移動領域を占有し、移動したつもりが拡大になる場合があった。選択する瞬間のタブ切替もキャンバス寸法を変え、進行中の移動を取り消す要因になっていた。

ユーザーにとって何が良くなったか

移動とサイズ変更を別の操作で行える。縫い順はボタンでもドラッグでも変えられ、保存・取り消しを共通経路で使える。基本設定の重複表示を減らした。

技術的な内容

  • ドラッグ開始位置は選択枠内を優先し、移動量を一時表示して終了時に一度だけ履歴へ記録する。タブ変更は終了後に行う。
  • 並べ替えは複数選択の相対順とグループ単位を保つ。挿入先のグループも分断しない。形や色の正本は変更しない。
  • アプリ専用のTransferableデータに文書ID・入力世代・編集版を持たせ、別文書や古いドラッグを拒否する。
  • 背景の針プレビューは読み取り処理なので、並べ替えを止めない。正本を変更する処理や描画中の操作は拒否する。

ハマったこと・学んだこと

ジェスチャーの当たり判定だけでなく、選択時のレイアウト変更が座標系や入力世代を失効させることも調べる必要があった。左一覧の上下操作は従来のprimary要素だけの移動から、選択集合全体の順序変更へ統一した。

UI刷新後の既存テストを、実際の設定経路へ追従する

test未リリース

UI刷新後の既存テストを、実際の設定経路へ追従する

リリース状況

未リリース。全件実行と修正対象の再検証を完了した記録。配布、Pencil実機、ミシンでの試縫いは未確認。

何を変えたか

既存のUI・E2Eテストを列挙し、CreationFlowTestsの53件とPencilPathFlowTestsの5件、合計58件を独立した3台の専用Simulatorへ分配して実行する。新しいリボンから画面外になる設定は、共通の設定メニューから開く経路へ追従する。テストの数値やUndo、保存再開の期待値は維持する。

なぜ変えたか

主要導線だけの確認では、既存の詳細編集や狭いウインドウ、実ファイル保存を網羅できない。UI刷新前の配置を前提とするテストが残っていたため、全件で確認する必要があった。

ユーザーにとって何が良くなったか

リボンでの基本編集だけでなく、詳細設定、保存、Undoの既存操作も継続できるかを確認する範囲が広がる。縫い順の糸切り距離入力も「完了」で閉じられるようになり、計算へ進める。

技術的な内容

SwiftUIアプリをXCUITestで実操作する。検証用の新規iPad Simulatorを作り、実文書とミシン向けファイルの保存先を検査する。別のPython QAは隔離した仮想環境へPillowとpyembroideryを導入し、依存定義から再現できるようにする。アプリの依存は増やさない。

詳細フォームは入力草稿の保持のため画面外にも存在する。全フォームを入力のたびに列挙する補助処理は、表示範囲と有効性を再確認しながら現在のフォームを再利用する形へ変える。固定のアクセシビリティ要素番号には依存しない。

利用者から実行時間短縮の要望があり、ビルドを一度だけ行い、そのxctestrunを3台で実行する方式を追加する。クラス単位の並列化では53件のCreationFlowに偏るため、ケースを名前単位で分配し、長いPaint・Photo・Sketchを別端末にする。端末内は直列のままで、結果束と保存データも独立させる。ビルドDBを同時更新しない。

全端末の結果をソースの全IDと照合し、不足・重複・スキップを成功としない。出力検査も当該端末群の明示的な保存先だけを集約して不足・重複を検出する。分配と結果集約の回帰テストを先に追加し、既存11件を含むPython15件が成功した。ソース列挙はサブディレクトリと非final classにも対応し、1ファイル複数XCTestCase等の非対応構成を黙って省略しない。全58件の3台実行部分は約81分、準備・ビルド等を含め約84分だった。ケース時間の合計は約3時間47分。比は約2.80だが、同条件の直列完走と比較した短縮率ではない。

Before / After

Before

設定の道具列にあるボタンを直接押す前提が残り、狭幅では画面外の設定を押せない。失敗時にtest本体のdeferが動かない場合、狭いウインドウが後続ケースへ残る。

After

共通設定メニューから設定を開き、setUpとtearDownでもウインドウを復元する。テスト間に端末状態を漏らさない。

判断したこと・トレードオフ

期待値を削除してテストを通すことはしない。共通の初期化を変えるため、対象ケースだけの再実行で完了とせず、修正ソースで全58件を再実行する。後続ケースへの影響も確認する。全件は47成功・11失敗で完走した後、影響する13件を修正・再検証し、未変更45件と合わせて全58IDの成功根拠を照合した。縫い順の距離欄だけは浮動decimalPadから数字・記号キーボードと完了キーへ変更し、その欄を使う保存再起動・PES出力E2E、全単体、Releaseを再実行した。他57件はこの欄を編集しない。最終バイナリで58件一括成功した結果ではなく、分割検証として記録する。

ハマったこと・学んだこと

  • SwiftUIのListは画面外の行を仮想化する。ある時点でexistsがfalseでも、識別子の不一致や機能欠落とは限らない。実画面、スクロール、最終結果を照合する。
  • アサーション中断後の後処理をtest本体のdeferだけに依存させない。失敗ログで実行された処理を確認する。
  • 複数の結果束をまとめるときは、ケース名とソースの同一性を照合する。実行回数と固有ケース数を混同しない。
  • キーボード非表示の操作を増やしても閉じない場合は、利用者自身が入力を終えられるUIかを見直す。距離欄はFocusStateと完了キーで明示的に編集を終了する。

表示中の縫い設定とPencil生成を一致させる

fix未リリース

表示中の縫い設定とPencil生成を一致させる

リリース状況

未リリース。実機で報告された不具合に対する修正。Simulatorと合成PencilKit入力で検証し、物理Pencil操作・試縫いの確認とは区別する。

何を変えたか

形を閉じても「内側は縫わない」を面埋めへ自動変更しない。手描き入力の細かな揺れを0.1mm以内で整理し、指定した線幅を保ったまま輪郭を生成する。十分な長さの一筆で終点が始点から0.8mm以内なら閉じた形として扱う。

現在の手描き/点入力モードを選択背景と枠、読み上げ状態で表示する。重複していた設定入口と「一つ戻す」を草稿行から外し、取り消す/やり直すを共通操作へ統合する。左右ドックは通常文字の1024pt以上で表示し、第9世代iPadの横1080ptでも使える条件にする。

なぜ変えたか

表示では内側を縫わないのに実際は面を縫う、253点程度の手描きが生成失敗する、現在の入力方法が分からないという報告があった。左右ドックも1180pt以上という条件により、第9世代iPadでは横にしても現れなかった。

ユーザーにとって何が良くなったか

画面に示した設定と生成結果が一致する。入力中の線や設定は共通Undo/Redoで戻せ、確定後は従来の文書Undoへ移る。手描きの揺れで失敗しにくくなり、横画面の一覧と詳細へ到達できる。

技術的な内容

PencilPathDraft.setClosedの自動スタイル変更を削除した。PencilKitから届く密な制御点は、既存の上限付きRDP簡略化で編集可能な中心線へ整理する。生成針へ平坦化せず、輪郭の自己交差・幅過大・過剰な仕事量の拒否を維持する。草稿の履歴と文書履歴を分け、入力中は草稿を優先する。非同期生成の古い結果拒否・失敗時の草稿保持も維持する。

Before / After

Before

閉鎖時だけ内側が面埋めへ変わり、確定後の空草稿では再び「内側なし」へ戻る。0.015mmの揺れを含む253点の半楕円で4mm幅の派生レールが折れ、生成できなかった。

After

表示中の輪郭/内側設定を閉鎖後も保持する。中心線を0.1mm以内で整理してから、同じ4mm幅で生成できる。幅やエラー条件を無条件に緩めない。

判断したこと・トレードオフ

手描きの正本は元のPencilKit制御点すべてではなく、0.1mm以内で整理した編集中心線となる。意図的に尖った形や幅に対して細すぎる部分まで生成可能とはしない。ドック表示時も中央524pt以上を確保し、狭幅やアクセシビリティ文字では上部入口へ退避する。

ハマったこと・学んだこと

  • 「縫わない」と表示するなら、形状の変化だけで自動的に方式を変更しない。
  • 高密度のPencilKit制御点と、幅のある線の派生レールに適した点列は同じではない。微小な方向変化が大きい幅では反転を生む。
  • 「横画面」の説明は機種ごとの論理幅と照合する。13インチだけで表示成功しても、10.2インチで使えるとは限らない。
  • 草稿の一つ戻すを削除するなら、共通Undoが文書を消さず草稿へ届くことも検証する。

手描きの曲線を正しく保持し、幅付きの線を生成する

bugfix未リリース

手描きの曲線を正しく保持し、幅付きの線を生成する

リリース状況

未リリース。ローカル実装と検証を進めている。App Store公開、物理Pencilでの最終操作、試縫いは未確認。前回の点列整理による修正後も実機で生成エラーが再発したため、入力と生成の両方を見直した。

何を変えたか

手描きの入力を曲線として保存する経路を追加した。通常の草稿表示では点番号を並べず、線と端点を表示する。幅付きの縫いは曲線に沿う断面から生成し、内側の仮の輪郭が重なったことだけでは全体を失敗にしない。

なぜ変えたか

PencilKitのPKStrokePathをCollectionとして列挙すると得られるのはB-splineの制御点で、実際の描画線上の点ではない。従来はこの点を直線で接続していた。点数を減らしても、入力の意味を間違えたままで、小さいS字などでエラーが残っていた。

生成側も、幅を付けた左右の輪郭から単純な帯が作れることを必要としていた。急な曲がりでは内側の輪郭が折れるため、曲線入力だけ直しても十分ではなかった。

ユーザーにとって何が良くなったか

指定幅を勝手に小さくせず、小さい曲線や折り返しから縫いを生成できる。内部の制御点をすべて番号付きで表示する状態を避ける。輪郭と内側の独立設定、草稿の取り消し、作品保存は維持する。

技術的な内容

  • iOS27ではPKStrokePath.bezierRepresentationを既存のBezierPath正本へ変換する。
  • 旧OSでは公開interpolatedLocation APIを使い、uniform cubic B-splineの各区間を4回評価して同じ3次多項式を復元する。制御多角形を線として扱う方法や、元の線と無関係な直線近似とは区別する。
  • 編集正本は曲線。針の出力に必要な離散化は生成時の派生処理に限定する。
  • 曲線の法線から左右の針位置を作り、曲線長と輪郭の移動量を基準に適応分割する。候補点と針位置を分け、曲線の区切りをまたいで指定間隔を持ち越す。区切りごとに針を置くとPencilの入力密度が縫い密度へ漏れるため、同じ直線の1区間/100区間で針数が変わらない回帰を追加した。幅・補正・ずらし・間隔を適用し、最大針長・針数・座標・処理量・取消を検証する。
  • 真の折り返しでは法線が不連続になる。片側の接線を採用し、空間的に十分小さい区間で分割を止める。反対向きの左右点を旧レール処理で再補間すると幅がゼロになるため、曲線断面は直接針位置へ変換する。
  • 吸着は線の端点に適用。内部の制御点をgridへ吸着させない。非同期結果は入力世代と草稿版が一致した場合だけ採用する。

Before / After

Before: 制御点を折れ線として受理し、0.1mmで間引いてから帯の輪郭を検証していた。

After: 実際の曲線を正本で保持し、生成時に曲線上の断面へ幅を適用する。旧VectorPathだけの図案には従来の生成経路を維持する。

判断したこと・トレードオフ

曲線の内側の重なりは許容するが、局所密度や生地上の仕上がりを自動保証する変更ではない。試縫いは別途必要。従来保存済みの折れ線には元のPencilKit情報が残っていないため、本来の筆跡を正確に復元したとは扱わない。

ハマったこと・学んだこと

合成PKStrokeを入力して成功しただけでは、実際の線を正しく解釈した証拠にはならなかった。PencilKit自身の補間位置と、変換後の曲線上の位置を直接照合するテストへ進める必要があった。曲線正本と最終針データの離散化を区別し、入力と生成の別々の誤りを調べることが重要だった。

検証上の注意

XcodeのiOS27 UI実行では、全ケースの終了後に任意のcoverage収集が停止し、未完成の結果束になった実行があった。失敗ログと未完成束を成功扱いせず保持し、coverageを無効にした再実行で最終の結果束を確認した。物理Pencil・試縫いは未確認のまま区別する。アプリ内のお知らせ機能は見当たらず、未リリースの変更を公開済みのお知らせとして追加していない。

閉曲線では、終点が始点と同じ座標でも端点としての識別を保つ。単一区間の閉曲線で終点を削除するとアンカーが1個になり、確定できなくなる境界があった。閉じるために追加した辺と入力に存在した区間を区別し、開き直した際にも元の曲線を失わないようにした。単一区間の確定・開閉・Undo/Redo・文書往復に加え、吸着ONの内部制御点保持と古い処理結果の不採用を回帰に加えた。

設定画面の往復を減らす刺しゅう編集リボン

ux未リリース

設定画面の往復を減らす刺しゅう編集リボン

リリース状況

未リリース。iPadネイティブアプリのUI刷新とローカル検証の開発記録。App Store配布、物理Pencil、ミシン試縫いの合格や本番提供を意味しない。

何を変えたか

Word型の上部リボンで、輪郭と内側を切り替えながら糸色・縫い方・幅・縫い目の間隔を変更できるようにした。広い画面には左に図案と縫い順、右に設定を置き、狭い画面では上部の入口から同じ編集へ進む。常設のガイドを初回だけのオーバーレイへ変更し、設定から再表示できるようにした。

なぜ変えたか

道具が上部にあっても、毎回大きな設定モーダルを開く構成では、色・幅・縫い方を続けて調整しにくかった。利用者から提供されたHTMLと配色定義を基準に、入口の位置だけでなく設定操作そのものを整理した。

ユーザーにとって何が良くなったか

キャンバスを見ながら基本設定を変えられ、変更ごとにUndoで戻せる。輪郭と内側の色を独立して保ち、線だけにする操作と面を埋める操作を区別できる。保存先や復旧は設定の作品情報へまとめ、制作中の表示を減らした。

技術的な内容

SwiftUIの共通トークンに青いアクセント、明暗の背景、境界、余白を集約。既存の編集正本・非同期針生成・Undo経路を利用し、画面整理のために文書形式や生成アルゴリズムを追加していない。クイック操作と詳細フォームの草稿は別の役割を持たせ、入力途中の詳細を無断で適用・破棄しない。

ハマったこと・学んだこと

  • 設定フォームを常設して入力を保持すると、未編集のフォームまで古い正本を表示し続ける可能性がある。元の設定と草稿を比較し、未編集のフォームは更新、入力途中のフォームは保持する境界が必要だった。
  • SwiftUIで親コンテナへ付けたアクセシビリティ識別子が子の識別子へ伝播するケースをSimulatorで確認した。コンテナをaccessibilityElement(children: .contain)で明示し、個々の操作を独立して特定できるようにした。
  • 常設フォームを画面外へ退避する設計では、XCUITestのfirstMatchが編集画面ではなく画面外の作成フォームを拾うことがある。画面内の実際に操作可能な対象を選び、見えている色番号と照合する必要があった。
  • 描画の確定を後にするUIでは、文書のrevisionだけでは草稿への採用を検知できない。既存の吸着処理は針文書が変わった場合だけマーカーを表示していたため、草稿のversionも照合するようにした。破棄時は処理を取消し、遅い結果で草稿が復活しないことも回帰テストにした。
  • 初回案内の表示条件は「まだ準備シートが開いていない」だけでは不足する。文書の復旧確認が終わる前にガイドが出て、後から準備シートが重なる問題を初回状態のUIテストで再現した。読み込み側が準備を許可した後にだけ自動案内へ進めるようにした。
  • 提供されたモックに接続済みや保存済みの文言があっても、実装と状態観測がなければ転記できない。Wi-Fi転送、筆圧連動、3D糸表示等の不足は、既存機能台帳へ別に記録した。

判断したこと・制約

Web版やデスクトップ版を追加せず、iPadのSwiftUIへ操作原則を移植した。参考のWebフォントと小さい操作寸法をそのまま移植せず、日本語の標準フォント、Dynamic Type、タップ領域、狭幅での到達性を優先した。左右のドックは十分なキャンバス幅を残せる場合だけ表示する。

手描きと点入力のパス作成、輪郭と内側の縫い設定をまとめる

feature未リリース

手描きと点入力のパス作成、輪郭と内側の縫い設定をまとめる

リリース状況

未リリース。実装とローカル受入を終え、独立レビューの指摘3件を修正し、再レビューに合格。App Store配信、Pencil実機の操作感、実ミシンでの縫製品質は確認していない。Gitのマージとアプリのリリースは別に扱う。

何を変えたか

Apple Pencilの手描きと、点を置いて直線・曲線をつなぐ入力を「パスを描く」にまとめた。点を結ぶ線は確定前の補助表示にし、始点へ接続した場合だけ閉じる。開いた形はサテン、閉じた形はフィルを自動の初期設定とし、確定後は「編集 → 縫い方」で変更できる。

1つの形に輪郭と内側の設定を持たせ、それぞれの糸色、ランニング・ビーン・幅付きサテン、面のタタミや下縫い設定を扱う。片方をなしにでき、ベジェの正本を保持したまま方式を変えられる。

なぜ変えたか

以前は自由な手描きをランニングとして即時追加し、曲線の追加はプリセットや数値編集が中心だった。描き始める時点の意図と、選択後の縫い方変更がつながっておらず、同じ形の面と輪郭を別色で扱うモデルも必要だった。

ユーザーにとって何が良くなったか

手描きと点入力を同じ場所から始め、確定前に形を調整できる。後から形を選ぶと、輪郭と内側の現在の縫い方を確認して変更できる。作成方法が違っても、同じ設定フォームを使える。

技術的な内容

Swift 6、SwiftUI、UIKit/PencilKitを使用。1つのオブジェクトIDと形状正本から、面・輪郭の生成用入力だけを一時的に作る。ベジェの制御点を保持し、面生成に使う点列を原本へ書き戻さない。色替えは各部の順に生成し、オブジェクト全体の糸切りを最後に置く。

作成草稿は文書と分離し、確定・方式変更を単一Undoで採用する。非同期生成の結果は、文書・入力世代・編集版・対象IDを照合してから反映する。取消や失敗では元の作品と入力を保持する。

Ink/Stitchの要素振り分けとInkscapeのパス操作の公開ソースを調査した。前者はGPL-3.0-or-later、調査した後者のファイルはGPL-2.0-or-laterの表記がある。ソースや素材を取り込まず、既存Swiftの形状処理を使って独立に実装した。

判断したこと・トレードオフ

  • 面と輪郭を別の文書オブジェクトへ複製すると、点編集で形がずれる。そのため正本形状を共有するモデルを選んだ。
  • 途中の点への接続や自己交差から領域を抽出せず、閉じられる接続先を始点だけにした。
  • 糸そのものの太さと、幅付きサテンの幅を混同しない。ランニングには見かけだけの線幅設定を付けない。
  • 作業中の別機能が保存形式35を使用していたため、新しいモデルは36を使用する。未対応の35文書を誤読して設定を落とさないよう拒否する。

ハマったこと・学んだこと

UIテストでドラッグ操作が完了しても、曲線が正本へ残ったとは限らない。スクリーンショットと保存JSONを照合して、直線のままになるケースを発見した。曲線区間数を読み上げ情報へ加え、操作後の状態まで検証するテストへ強化した。

常設しているSwiftUIフォームは、非表示の内容もXCTestの候補へ現れることがある。同じラベルの最初の要素を押すだけではなく、画面内の実操作可能な要素を選び、必要なら実際にスクロールする必要がある。

UIKitのジェスチャー認識時には区間上にいた指が、最初のドラッグ通知までに区間の当たり判定範囲を出ていた。実測ログでは認識時の移動量6ptに対し、開始通知の移動量は0へリセットされていた。認識時の区間・形状・原点を保持して操作の終わりまで使い、開始時の再判定をやめると、同じ実操作の回帰テストで曲線が確定された。

作業中の文書起動・縫い順・自動糸切りをパス編集と統合する

improvement未リリース

作業中の文書起動・縫い順・自動糸切りをパス編集と統合する

リリース状況

未リリース。Gitのmainへの統合とApp Store配信を分けて扱う。実機Pencil、試縫い、最小OS17、署名とストア提出は未確認。統合のローカル動作確認を完了した。mainへの反映とレビュー記録は関連PRで追跡する。

何を変えたか

未コミットだったネイティブ文書ブラウザ、UIDocumentの保存セッション、外部更新の競合処理、縫い順最適化、自動糸切りを、Pencilによるパス作成と輪郭・内側の縫い設定へ統合した。過去のQA資料・測定スクリプト・公開準備台帳も開発履歴として保持する。

なぜ変えたか

機能が別々の作業状態で残っており、最新mainへ移るだけでは以前の作業が使えなくなる。退避を最終結果にせず、保存形式と生成・保存・起動をそろえて利用できる状態へまとめるため。

ユーザーにとって何が良くなったか

通常の文書ブラウザで作品を作成・再開し、手描きや点入力で編集したパスを、縫い順比較・Undo・保存・ミシン用出力まで同じ作品で扱える。最適化は重なりや固定条件を守り、面と輪郭の内部順序を保つ。自動糸切りの距離設定と並べ替えは一回のUndoで戻せる。

技術的な内容

Swift6、SwiftUI、UIKit UIDocument/UIDocumentBrowserを使う。編集用文書の正本と針出力を分離したまま、UIDocumentのsnapshot境界をロックで保護し、MainActorの編集セッションで保存・閉じる・外部読込との競合を扱う。

保存形式35の自動糸切りと、36のPencilスタイルを、新しい37へ移行する。既知の35/36データを保持して読み、36が知らなかった自動糸切りを36へ混ぜた入力は拒否する。旧版が未知の設定を落として保存する経路を作らない。

アプリ識別子は既存のjp.stitchatelier.appを維持する。起動処理の変更時にbundle IDまで変えると別アプリとして扱われ、既存文書への導線が分かれてしまう。診断中の別識別子と製品の識別子を混同しない。

ハマったこと・学んだこと

保存形式の競合を片側だけ採用すると、もう一方の機能が読み込めなくなる。競合ハンクを監査し、コードが同じで番号だけ異なることを確かめて両方を扱う形式へ統合した。元の163ファイルはローカルブランチへ保存し、保存前後のSHA256一致を確認した。

SwiftUIのDisclosureGroupへ親のアクセシビリティ識別子を付けると、子のスイッチやボタンの識別子まで上書きされる場合があった。識別子をラベルへ限定し、各操作を独立に確認できるようにした。表示できたことと操作対象を正しく識別できることを分けて検証する。

適用した保存値を入力フォームへ同期する処理と、ユーザーの未適用入力を別扱いにする必要もあった。同期を入力変更として処理すると成功通知まで消えるため、入力Bindingの更新時だけ比較を取り消すようにした。計算結果の非同期挿入時にSwiftUI Listの内部整合性例外も検出し、進捗・比較結果・通知を安定した一つの行へまとめた。

独立レビューでは、ローカル編集が成功した後に外部版を再読込する順序の取りこぼしが見つかった。現在値だけで比較せず、最後に読み込みまたは保存が確定した版と、退避する未保存ローカル版を区別する。競合中は保存と閉じる処理を止め、両版を選べるようにした。保存成功時の基準も、その瞬間の現在値ではなく実際に書いたpackageから更新し、保存中の後続編集を守る。

閉じる処理中はUIへの外部更新通知を止めるため、失敗して編集へ戻るときに保留中の状態を再確認する必要がある。開始直前にも外部状態を同期し、競合選択への入口を失わないようにした。OSの変更カウントがクリアされても退避中のローカル版は未保存として扱い、保存を省略して閉じないようにする。

制約を保持する縫い順最適化と自動糸切り

feature未リリース

制約を保持する縫い順最適化と自動糸切り

リリース状況

未リリース。コード実装と自動試験は完了。通常の起動画面から編集画面へ進めないため、Simulator操作導線の最終受入は未完了。実機試縫いも未実施。他の部分実装・未実装機能は保留。

何を変えたか

重なり・グループ・レイヤー・ロック・非表示・停止を保持する縫い順候補を計算し、色替え・糸切り・ジャンプ・距離の前後比較から適用できる実装を追加。自動糸切りの距離閾値を保存形式35で保持。設定と順序を一度のUndoで戻せる。

なぜ変えたか

単純な色順ソートでは重なりや停止の意図を壊すため、制作上の順序制約を先に定義する必要があった。

ユーザーにとって何が良くなったか

適用前に指標を確認でき、取消時は作品を変更しない。保存した閾値をプレビューと機械形式の出力で共用する。

技術的な内容・学んだこと

編集正本を保持し、生成針の外接矩形と実糸情報から先行制約を構築。候補の再生成後にオブジェクト別針位置・糸情報、明示糸切りと停止を検証する。自動糸切りを加える前の命令列も比較し、明示指定と自動追加を区別する。非同期結果は文書IDとrevisionで失効する。

判断したこと・制約

全順列の最適解は保証しない。色替え、糸切り、ジャンプ数、距離を順に優先するため全指標の同時改善も保証しない。探索の代理指標で絞る際、別の糸切り改善候補を取りこぼす可能性がある。重なり判定は外接矩形による保守的な判定。

検証

Core全888件、App全456件成功。その後に複数色Crossと連続取込、最適化後PES/DST/JEFの針位置・色替え往復試験を追加し、Optimizer14件成功。通常Release Simulatorビルド成功。静的レビューで確実なP1/P2指摘なし。操作画面の受入と試縫いは成功扱いしていない。

作品を変更せずに2点間を測るキャンバス計測

feature未リリース

作品を変更せずに2点間を測るキャンバス計測

リリース状況

未リリース。標準画面での操作と自動テストを確認した開発版。最大文字・狭幅・VoiceOver・物理Pencilの受入は残る。

何を変えたか

確認タブに2点間の直線距離計測を追加。キャンバスの2回タップまたは始点/終点の座標入力で測り、mm・cm・inchで表示する。3点目は新しい計測の始点になる。測り直しと終了も用意した。

なぜ変えたか

図案全体の外接寸法だけでは、部分的な間隔や任意の2点の距離を調べられなかったため。

ユーザーにとって何が良くなったか

図案や針を追加せず、その場で距離を確認できる。計測は作品のUndo履歴を消費せず、通常編集へ戻った後も直前の図形操作を取り消せる。

技術的な内容

作品のmm座標で2点を保持し、hypotで距離を計算する。一時状態は文書schemaへ保存しない。入力世代と表示領域IDで古いタップを拒否し、数値4項目は検証後に一括反映する。不正値では前の結果を保持する。生成・取込中や未確定の針/穴入力との競合を防ぐ。

距離表示はキャンバスへ重ね、表示開始でキャンバス寸法が変わらないようにした。独立レビューで見つかった空作品の素材選択カードによる操作遮断と、各ボタンの44pt未確保も修正した。

ハマったこと・学んだこと

右寄せの長い座標をテストで置き換えるとき、中央タップではカーソルが先頭に置かれ、後退削除で文字が残った。入力後の結果だけでなく、全消去後の空文字と入力後の値の一致を検証した。最終テストではCommand+Aを使用する。単位選択のAXラベルは現在値を含むため、表示名ではなく専用識別子を使った。

最大文字でも計測する場所を残す

fix未リリース

最大文字でも計測する場所を残す

リリース状況

未リリース。計測の追加受入で見つけた表示問題を修正した。数値フォーム全項目の最大文字・狭幅受入、VoiceOver、物理Pencilは残る。

何を変えたか

計測パネルをキャンバス高さの35%以内に収め、自然な高さで収まらない場合は内部スクロールへ切り替える。文字サイズと各ボタンの44pt以上は維持する。

なぜ変えたか

最大文字サイズでは計測パネルが中央まで広がり、計測点を指定するタップが設定ボタンへ入っていた。標準文字の成功だけでは見つからなかった問題だった。

ユーザーにとって何が良くなったか

文字を大きくしても中央の2点を測り、パネル内をスクロールして終了できる。狭いウィンドウでも計測操作を維持する。

技術的な内容

SwiftUIのViewThatFitsで自然高とScrollViewを切り替える。パネルだけを制約し、作品座標や計測距離、キャンバス自体の寸法は変更しない。

ハマったこと・学んだこと

各ボタンが大きく表示されても、パネル全体が入力面を塞いでいれば操作できない。文字サイズ、操作領域、入力面を同時に検証する必要がある。狭幅の初回テストは計測前の文書オープンで止まったため、表示不具合と同一視せず起動調査へ分離した。

点編集・手動針・描画の吸着と、一時表示を壊さないキャンバス管理

feature未リリース

点編集・手動針・描画の吸着と、一時表示を壊さないキャンバス管理

リリース状況

未リリース。グリッド・ガイドへの吸着を点列、ベジェ制御点、手動針、描画へ接続した。Simulatorと自動試験で検証した範囲を記録する。物理iPad、Pencil、試縫い、署名配布の確認は残る。

何を変えたか

点の直接編集と手動針は画面上8ptの距離を作品座標へ換算して吸着する。数値入力は指定値をそのまま使う。描画の吸着は専用設定で有効化し、計算中の取消と失敗後の再試行に対応した。

図案移動、点列・ベジェのドラッグ、手動針と描画終点に吸着位置の丸と十字を表示する。表示は編集正本やUndoへ保存しない。描画終点は文書への採用が成功した場合のみ表示し、次の描画開始や表示変更で消す。

なぜ変えたか

図案全体の移動だけが吸着しても、細かな点編集や線の作成では基準を使えない。吸着先も見えず、操作結果を判断しにくかった。

ユーザーにとって何が良くなったか

同じグリッド・ガイドを作成と再編集に使い、吸着位置を確認できる。描画の追加によって制作ガイドの高さが変わり、キャンバスがずれる問題も修正した。

技術的な内容

固定ガイドの交点を準備し、重複座標を元の優先順位でまとめ、KD-treeで検索する。追加グリッドとの交点は点ごとに評価する。交点が許容距離にない場合だけ投影を使う。固定ガイドの方向ベクトルも準備時に保存して再計算を減らした。

操作開始時の座標変換・入力世代・表示領域と文書revisionを保持し、古い結果を拒否する。ドラッグ中の仮表示は確定時の検証と共通化し、文書やUndoを変更しない。非同期描画の計算成功と文書への採用成功を分けて扱う。

ハマったこと・学んだこと

モデルでは終点マーカーを保持できても、図案数の変更時に閉じた詳細画面を再び閉じる処理が表示領域を失効させていた。それだけを直してもUI試験は失敗した。制作ガイドが素材から編集へ切り替わり、高さが変わる別の経路が残っていたためだった。

連続描画中は素材段階を維持し、空から最初の図案を作る場合も説明文を図案数に依存させないようにした。画面試験ではマーカーの存在だけでなく、描画前後のキャンバスframeが完全一致することも確認した。

吸着失敗後に吸着設定をオフにして再描画すると、以前のエラー表示だけ残るケースも回帰試験で再現し、吸着しない入力の受付時に以前の処理状態を解除した。

Sketch Stitchを画面から作成・再編集できるようにした

feature未リリース

Sketch Stitchを画面から作成・再編集できるようにした

リリース状況

未リリース。専用画面の実装とSimulator検証を完了。物理iPad、Pencil、実写真の縫製品質、ミシン試縫い、署名配布は未確認。

何を変えたか

FilesのPNG/JPEG入力からSketchを生成し、作成後に設定・糸色を編集する画面を追加した。色数・線間隔・ブラシ幅を基本設定に置き、針長・平滑化・混色・明るさ・コントラスト・解析解像度を詳細設定に分けた。新規と対象別の未適用入力を保持し、生成失敗や取消で入力を消さない。

なぜ変えたか

生成Coreと編集文書だけでは利用者が制作できない。画像取込から設定変更、保存、再開、ミシン用出力まで通常操作でつなぐ必要があった。

ユーザーにとって何が良くなったか

下絵を削除した後も保存元PNGから再解析できる。解析色と表示糸色を分離し、糸色だけの変更では針位置を変えない。黒・灰色の登録糸を解析に使い、実際に縫う色はHEXや登録糸から変更できる。大きな文字や狭いウインドウでも設定・更新操作へ到達できる。

技術的な内容

画像処理・非同期採用・草稿・SwiftUIフォームを分離した。文書や対象が変わった後の古い処理結果を拒否し、成功時だけ一回のUndoとして採用する。解析に使える灰色の判定はCoreと画面で共有する。

ハマったこと・学んだこと

常設フォームは非表示でもアクセシビリティ階層に残るため、同名要素の最初の一件を探すUI試験は画面外の入力を拾った。表示中のモーダル内へ検索を限定した。また浮動ウインドウではApplication.frameとWindow.frameの原点が異なり、フォームの画面座標との比較にWindow.frameを使う必要があった。画面証拠も画面全体から取得するよう変更した。

繰り返しインストールしたSimulatorではFilesの保存先参照エラーが発生した。文書生成自体は成功しており、データ消去を行わず専用Simulatorを再起動すると同一ビルドの試験が成功した。アプリのシリアライズ不具合と混同せず、試験環境の状態として記録した。

Sketch Stitchの編集文書・保存・出力を接続

feature未リリース

Sketch Stitchの編集文書・保存・出力を接続

リリース状況

未リリース。文書統合の実装と自動試験、既存編集導線のSimulator試験、署名なしReleaseビルドを確認。Sketch専用制作画面と実機縫製は未完了。

何を変えたか

Sketchの元PNG・解析RGBA・生成設定・配置・糸色をschema33の編集文書に保存する。複製、移動、変形、Undo/Redo、使用色による選択、テーマ配色、PES/DST/JEF/EXP出力へ接続した。

なぜ変えたか

生成した針だけを保存すると、画像の解析条件を変更できなくなる。編集可能な画像正本を残し、Photo/Paintと混在した作品でも再編集できる基盤が必要だった。

ユーザーにとって何が良くなったか

専用画面を接続した後も、下絵の削除によって元画像を失わず、保存した生成条件から再編集できる。今回は文書と処理の統合段階で、画面からSketchを作れる状態とは区別する。

技術的な内容

元PNGを共有プールで管理し、最後の参照を削除した時だけ破棄する。解析色と表示糸色を分離し、解析RGBを糸色上書きのキーとする。配色変更で針位置を再分類しない。通常の矩形ノード編集や単一糸割当による画像正本の破壊も拒否する。

ハマったこと・学んだこと

Sketch専用の配色分岐を追加した際、後続の通常オブジェクト用分岐にも進み、登録糸の属性が不正に設定された。登録糸を使う再現テストで失敗を確認し、排他的な分岐へ修正した。新しい方式を追加する時は、成功時の色だけでなく、他方式の属性が混入しないことも検査する必要がある。

Sketch Stitchの生成Coreと方向に応じた細分化

feature未リリース

Sketch Stitchの生成Coreと方向に応じた細分化

リリース状況

未リリース。生成Coreの実装・自動検証・独立レビューまで。保存・制作画面・ミシン用出力への統合、実写真・実機試縫いは後続。

何を変えたか

画像の濃淡から黒・灰色の短い線を生成する独自Sketch方式を追加。色数、糸パレット、間隔、線幅、針長、平滑化、混色、明るさ・コントラストを扱う。

なぜ変えたか

画像から編集可能な刺しゅうを作る機能の拡充。輪郭抽出だけでは暗部の階調を表現できないため、濃淡に応じて線の採用量を変えた。

ユーザーにとって何が良くなったか

将来の専用制作・再編集画面へ接続する生成基盤ができた。現時点で利用者向け機能の提供完了とは扱わない。

技術的な内容

SwiftでRGBAと設定から決定的な色別runを生成。透明/白の区間分断、アフィン配置、操作予算、取消、座標を保持しない計数モードを実装。方向の変化に応じて8mm領域を最大1mmまで分割する。

ハマったこと・学んだこと

  • 固定格子上で線だけ回転すると、縦方向で同じ場所を繰り返し縫った。独立座標検査では勾配素材の重複線長が約53〜66%。方向に沿う平行線配置と排他的領域へ変更後、同素材の共線重複は0mmになった。
  • 幅付きの線は中心線の所有だけを決めると境界で片側が欠ける。候補領域を半幅拡張し、最終線だけを担当領域へ切り分けた。
  • 勾配の符号付き平均は山谷で相殺される。180度同値の方向分布を使い、曲線追従を改善した。
  • 座標安全性のテストだけでは表現の角張りを検出できない。生成描画と解析接線の角度差を別に測った。

判断したこと・トレードオフ

独自放射状素材の方向誤差95%点は21.41→6.55度、波形は18.59→7.56度。一方1mm未満runは約30〜36%に増えた。方向精度の改善を縫製品質の保証とせず、短runとJumpの評価を残した。

Paintの制作・再編集画面と編集草稿の保持

feature開発中(未リリース)

Paintの制作・再編集画面と編集草稿の保持

リリース状況

開発中・未リリース。制作・再編集画面を実装し、通常Files入力から保存・再起動・PES出力までのSimulator受入が成功した。実写真の縫製品質、物理iPad・Pencil、実機ミシンは未確認。

何を変えたか

下絵画像の変換方式にPaintを追加し、作成済みオブジェクトから専用編集画面を開けるようにした。基本設定は色数・間隔・針長。詳細には平滑化、混色、明るさ、コントラスト、解析解像度、被写体の色を優先する範囲を配置した。範囲の中心は画像タップと数値の両方で指定できる。

解析色と実際に縫う糸色を分けて表示し、一色ずつカラー・HEX・登録糸で変更する。設定更新と糸色適用を別の操作とし、新規作成用・各オブジェクト用の未適用入力を保持する。

なぜ変えたか

生成や保存のAPIだけでは利用者が通常の制作導線から使えない。また、設定を試しながら画面を閉じたり対象を切り替えたりしたときに入力が消えると、調整をやり直す必要がある。

ユーザーにとって何が良くなるか

画像からPaintを作り、作成後も色数や間隔を調整できる。下絵を削除しても保存元画像を使って再編集する。糸色だけを変える場合には針位置を保持し、画像の解析条件を変える操作と区別できる。

技術的な内容と学び

画像復号と針生成は非同期に実行し、文書・選択・編集世代を照合して結果を採用する。取消や別文書への移動後に古い生成結果を適用しない。

草稿の同期では、正本で変わったフィールドだけを反映する。登録糸のメタデータだけをUndoしたとき、未適用のColorPicker色まで消えるケースを回帰テストで検出した。確定色と糸情報を別々に比較し、無関係な途中入力を残すよう修正した。

画像プレビューは条件分岐を囲む固定コンテナに非同期タスクを付けた。変更前には読み込み表示が終わらない状態をUIテストで再現し、変更後は画像と範囲表示を確認できた。タスク再開始回数は取得できていないため、その内部原因まで断定していない。

UI試験では、SwiftUI Listの画面外の入力欄が遅延生成される。存在待ちを先に行うだけでは下方の欄に到達できず、基本設定へ戻る場合にはスクロール方向も明示する必要があった。

再解析の試験画像は160×120画素を使い、初期128×96と更新後64×48を区別する。元から64画素の画像では、解像度指定を無視する不具合を検出できない。また、Files取込ではPNGを正規化するため、入力ファイルの圧縮バイト同一性を画素保持の条件にしてはいけない。独立した画像デコードと保存元PNGのハッシュを使い分ける。

検証

App361件・Core538件、計899件が成功。独立仕様・品質レビューで検出した草稿同期の問題は修正済み。実機向けReleaseの署名なしビルドも成功した。

Paintの通常操作試験1件が成功。元PNGの保持、初期128×96から64×48への再解析を実保存文書で確認した。PESを独立readerで読み、保存正本から再生成した5,582針・15色の全座標と色順が一致した。既存Photo制作の回帰1件も、保存再起動・再解析・PES保存まで成功した。通常PES出力の対象回帰も成功。アプリのローカルコミットは5db7e29。実機の縫製品質や競合製品と同等の写真表現を保証するものではない。

数値入力のUI試験では、浮動式の数値padをEscapeで閉じたつもりでも、次の外側タップが入力終了に使われるケースがあった。数値の位置で長押し・全選択し、入力値を照合後、フォーム余白で入力を終了してから閉じる手順で通過した。テスト実行ログとソースが一致しない試行もあったため、以後はxctestrunを明示し、runnerのハッシュと操作識別ログを確認した。内部原因は未確定であり、Xcode一般の不具合と断定しない。

刺しゅう図案の配置用グリッドとガイドを文書へ保存する

feature未リリース

刺しゅう図案の配置用グリッドとガイドを文書へ保存する

リリース状況

未リリース。配置補助のうち、設定・表示・図案移動時の吸着・ガイドの直接移動と回転を実装した。点編集、手動針、Pencilへの吸着、吸着先マーカー、定規などは未接続。実機Pencil・試縫い・署名配布は未確認。

何を変えたか

格子の縦横間隔・原点、名前付きの水平・垂直・任意角度ガイドを保存できるようにした。表示と吸着は独立切替とし、ガイドには個別の表示・ロック・削除を用意した。図案を動かす際は選択枠の角・辺中央・中心から補正量を求め、複数選択全体に同じ移動を適用する。

ガイドは表示設定から専用編集へ入り、中心ハンドルで移動、回転ハンドルで角度を変更できる。数値入力も残し、操作終了時に一度だけUndoへ記録する。

なぜ変えたか

枠の表示と基本整列だけでは、制作途中の任意の位置・角度を基準に複数の図案をそろえにくかった。配置基準を作品と一緒に保存し、開き直しても同じ基準で編集できるようにするため。

ユーザーにとって何が良くなったか

ガイドの名前やロックで配置基準を管理できる。拡大表示でも吸着の操作距離を画面上で一定に保つ。数値移動には吸着を適用せず、指定値を尊重する。補助線はミシン用の針に含まれず、ガイドだけを編集しても既存の針データは変わらない。

技術的な内容

schema34の文書メタデータとして保持し、編集正本・生成針とは分けた。吸着はCoreの幾何処理へ集約し、交点と垂足を同じ許容距離で評価する。格子の描画は可視範囲に制限し、細かい格子を間引く場合も吸着の元間隔は変えない。

直接操作では開始時のガイド、文書revision、入力世代、表示領域、操作IDを固定する。仮表示を文書から分け、古い終了通知を拒否する。点編集・描画・文書置換でモードを解除し、SwiftUIのGestureStateのリセットから認識器の取消も処理する。

ハマったこと・学んだこと

完全一致の吸着点を「補正がない」として候補から外すと、既にそろった図案が近傍の別候補へずれる。候補なしと完全一致をOptionalで区別し、距離ゼロを最優先にした。

補助メタデータの更新を通常の図案変更と同じ経路へ入れると、不要な針再生成が発生した。ガイド情報以外が同じ場合は生成結果を保持し、品質指摘の文書revisionだけを更新した。Undo/Redoでもこの境界を確認した。

UI試験では、SwiftUIのリストで一部だけ見える行がisHittableでも、タップ先がモーダルのクリップ範囲外にあった。対象全体が表示中フォームに収まるまでスクロールするようにした。テスト件数0の実行も成功証拠にはせず、Xcodeのテスト列挙と実行件数を照合した。

Paintの画像正本と糸色を保存・出力へ接続

feature開発中(未リリース)

Paintの画像正本と糸色を保存・出力へ接続

リリース状況

未リリース。Paint生成Coreを文書・編集・出力へ接続した段階。通常の画像取込からPaintを作成・再編集する専用画面は後続で、Paint機能全体の提供完了ではない。

何を変えたか

元PNG、解析RGBA、配置、生成設定、解析色別の糸色上書きをschema32へ保存する。Photoと同じ元PNGを共有し、最後の参照が消えるまで保持する。Undo、変形、条件選択、テーマ配色、プレビュー、PES/DST/JEF/EXP出力へ接続した。

なぜ変えたか

生成針だけを保存すると、色数や混色を後から調整できない。元画像と設定を編集用の正本として保存し、針データはそこから生成する必要がある。

ユーザーにとって何が良くなるか

制作画面の接続後は、下絵を削除してもPaintを再編集できる。糸色変更では針位置を維持し、移動・拡縮後も実寸の間隔から生成し直せる。編集用作品とミシン用ファイルを分けて保存する構成を保った。

技術的な内容と学び

解析に使うパレットを直接糸色へ置き換えると、画素分類まで変わって針の場所が変化する。そのため解析元RGBをキーとした上書きを持ち、色だけを生成後に適用する。明るさなどの変更で解析元色が消えた場合は、その色の上書きを除く。密度や配置だけの変更は割当を保持する。

画像を共有するときは、保存容量だけでなく参照寿命も複数方式で管理する必要がある。PhotoとPaintの参照を合わせて画像を保持し、削除や針への変換をUndoすると元画像も戻す。PNG完全復号はアプリの読込・復旧境界で検証する。

検証

App/Core877件成功、独立仕様・品質レビュー済み。レビューで不足したモデル経由の変形・一般編集拒否と、PES/DST/JEFの色ブロック別全針座標比較を追加した。座標は0.1mm量子化、色はPES RGB・JEF近似パレット・DST色情報なしの違いを検証した。

Simulatorの条件選択・既存図案の編集・Undo・保存再起動1件が成功。これはPaint制作画面の試験ではない。generic iOS向けRelease署名なしビルドも成功。物理iPad・最小OS・実写の見え方・試縫いは未確認で、後続の受入対象。

Photo Stitchの作成・再編集画面と対象別の入力保持

feature開発中(未リリース)

Photo Stitchの作成・再編集画面と対象別の入力保持

リリース状況

未リリース。作成・専用再編集フォームを実装し、Core/App全835件・通常操作のSimulator受入2件・Release署名なしビルドが成功した。独立仕様・品質レビューを通過。アプリのローカルコミットは951572e。

何を変えたか

下絵の画像変換へPhotoを追加し、選択したPhotoを編集リボンから再編集できるようにした。単色/CMYKと糸色を基本設定、帯間隔・暗部間隔・ガンマ・反転・解析解像度を詳細設定にまとめた。初期の描画モードからは、描画を終了して作成へ進む明示的な操作を提供する。

なぜ変えたか

画像からPhotoを生成する処理だけでは、利用者が設定を調整して作品を完成させられない。新規作成と作成済み対象の編集を分け、対象を切り替えても未適用の設定を失わない制作導線が必要だった。

ユーザーにとって何が良くなったか

新規用と対象別の入力を独立して保持し、設定を閉じてキャンバスへ戻った後も入力を続けられる。保存済み画像の解析設定は維持を既定にし、利用者が解像度を変更した場合だけ再解析する。下絵を削除した後も、Photo自身が保持する元画像で更新できる。

技術的な内容

フォームは常設し、草稿をオブジェクトIDと文書の識別子で管理する。草稿と最後に読んだ正本を比較し、Undoや別の配色操作で変化したフィールドだけ同期する。色と登録糸の情報は一つの選択として扱い、任意色を適用したスロットだけ糸割当を外す。

生成結果の完了通知ではなく、実際に文書へ採用されたrequest IDと、自フォームが開始したIDの一致を閉じる条件にした。失敗・取消・古い結果では、成功したような画面遷移を起こさない。

ハマったこと・学んだこと

親フォームを常設しても、内部の条件分岐で子Viewが作り直されると、その子の状態は消える。仕様レビューではHEX入力の文字列が共通部品内の状態に残っており、別対象への選択変更で消失したり、同色の別対象へ混入したりする問題を見つけた。

PhotoではHEX文字列と検証エラーも対象・糸スロット別の草稿へ移した。文字を入力するだけでは糸色を変えず、HEX適用で色を確定する操作は維持している。対象往復、文書置換、単色とCMYKの切替、正本色変更に対する同期を追加試験した。

通常画面の試験では、折りたたみ設定の親へ付けたアクセシビリティ識別子が子の入力欄へ伝播し、各数値欄の識別子を覆うことも確認した。親の識別子を外し、見出しは日本語ラベル、数値欄は個別識別子で確認するようにした。画面に表示されていることと、自動操作から各欄を区別できることは別に検証する必要がある。

検証と未確認範囲

Core502/App333の全835件成功、失敗・スキップ・実行時警告0。仕様レビューのHEX保持指摘を修正し、再レビューと独立品質レビューに残指摘なし。通常Filesから単色Photo生成、草稿保持、移動Undo/Redo、下絵削除、保存終了・再起動、元画像の再解析、PES保存を通した。既存PES保存との対象UI2件は成功し、全UIカタログの再実行とは区別する。

保存実ファイルもガンマ1.2、解析64×48、下絵なし、元画像参照保持を照合した。PESはPhoto 18,594 bytes、既存1,312 bytesでヘッダーとPEC参照を検査した。画面で選択できることに加え、永続化された設定と解析結果を確認することで証拠を補った。

実機向けRelease署名なしビルドも成功。CMYKの全UI操作、最大文字サイズ・画面回転・VoiceOver、iPadOS17実行、物理iPad/Pencil、実写真の縫製再現性、ミシン試縫いは未確認として残る。

写真の色を短い縫い線へ変換するPaint Stitch Core

feature開発中(未リリース)

写真の色を短い縫い線へ変換するPaint Stitch Core

リリース状況

未リリース。画像から色別の縫い線を生成するCore段階。アプリ画面・文書保存・ミシン出力への接続は後続であり、Paint Stitch機能全体が利用可能になった意味ではない。

何を変えたか

写真の階調を、色を混ぜた短い縫い線で表現する独自生成処理を追加した。色数、糸パレット、間隔、針長、平滑化、混色の強さ、明るさ・コントラスト、被写体領域の重みを持つ。原画像を変更せず、設定と配置が同じなら同じ結果を返す。

なぜ変えたか

色領域を単色のタタミにする変換と、写真の中間色を糸の混合で表現する変換は目的が違う。Photoのサテン帯とも別に、短い縫い線の向きと色の分布を調整できる生成方式が必要だった。競合は公開機能の参考に留め、コードや収録図案は取り込んでいない。

ユーザーにとって何が良くなるか

文書・UIへの接続後は、画像を元にしたPaintの色と質感を設定から再生成できる。指定した糸パレットからの色選択や、被写体の色を優先した減色も可能になる。現段階ではCoreの実装であり、この制作画面はまだ提供していない。

技術的な内容

色補正と透明度を重みにした平滑化の後、被写体領域を重く扱うヒストグラムから色を選ぶ。近い2色の間で座標由来の固定値を使って糸を選択し、中間色を空間的に表現する。線の向きは輝度勾配の接線から求め、画像のアフィン配置を通して実寸の短いstrokeにする。

透明な1画素の帯を端点だけで見逃さないよう、線分が横断する画素境界で区間を分ける。透明区間の両側を縫い線でつながず、別runにする。後段の縫い計画ではrun間をジャンプとして扱う必要がある。

ハマったこと・学んだこと

針数上限だけでは、透明領域が多く針が出ない入力の計算量を制御できない。格子数と色距離・画素区間の処理量にも予算を設定した。針配列を保持しない数え上げでも、通常生成と同じ色選択・区間分割・上限検査を行う。

選択したパレットと実際に使った色を分けることも必要になる。未使用の選択色を保持しつつ、出力の色層には針がある色だけを含めることで、後段の再編集と不要な色替え回避の両方へ接続できる。

検証と未確認範囲

未実装APIの失敗を先に確認し、対象18件と既存を含むCore全520件が成功した。99色・指定糸・focus・混色・透明帯・変形・針長・数え上げ一致・上限・取消を検査。文書保存、編集UI、実写の見え方、最小対応実機の性能、Pencilと試縫いは後続の受入対象。

独立仕様レビューの後に品質レビューを行い、修正必須の指摘なし。実機向けRelease署名なしビルドも成功した。処理途中の取消はループのコード確認に留まり、今回の取消テスト自体は開始前取消である。

縫い方と使用糸色から刺しゅうオブジェクトをまとめて選ぶ

feature開発中(未リリース)

縫い方と使用糸色から刺しゅうオブジェクトをまとめて選ぶ

リリース状況

未リリース。条件による一括選択を実装し、仕様・品質レビュー、全App/Coreと対象Simulatorの検証を完了。実機・Pencil・ミシン試縫いの受入は最終工程として残す。

何を変えたか

図案一覧へ「条件で選択」を追加し、縫い方式または使用糸色から選択を置き換える・追加する・除外する操作を提供する。糸色は面・輪郭・すべてに分けて探せる。登録糸や取込データの使用糸名をHEXと併記し、名前のない任意色はHEXで表示する。

なぜ変えるか

複雑な作品で同じ色や同じ縫い方の図案を一つずつ選ぶと、選択漏れが起きやすい。現在の複数選択やグループ編集へ、条件から対象を集める入口を追加する。

ユーザーにとって何が良くなるか

一致数とグループを含む選択対象数を見てから既存の移動・整列などの操作へ進める。選択しただけでは文書やUndo履歴を変更しない。複数色や複数方式を持つ図案は分解せず、編集できる正本を保持する。

技術的な判断

「パレットに存在する色」と「刺しゅうで使っている色」は同じとは限らない。Crossでは色が割り当てられたマスから使用色を求める。取込針では色替え命令でスロットを進め、実際の針落ちがあるスロットだけを対象にする。ジャンプだけのスロットや未使用の登録糸を一致に含めない。

領域分割では、分割前の親に残した復元用の設定と、現在縫われる葉領域の設定を区別する。葉のどれかが一致したときに元オブジェクトを選ぶ。サテン領域だけを切り離す処理にはしない。

設計から得た考察

同じ色をまとめる基準は、糸メーカーや番号ではなく表示RGBのHEX値とする。画面で同じ色として見える対象を集める目的と、製品として同じ糸を探す目的を区別する。

グループは一致した一員から全体へ展開する。非表示・ロックを除外する場合も、一員だけを選ぶのではなくグループ全体の可否を判定する。条件で選ぶ入口だけが既存の編集単位を壊さないようにする。

実装で学んだこと

条件による選択も、既存の選択操作と同じモード切替が必要だった。主対象を維持したまま選択集合を増やすと、主対象の変更監視だけでは点編集を終了できない。適用成功時に点編集を終了し、失敗時は元の状態を保つ回帰テストを追加した。

色の一致キーと表示名は分ける。表示名を一致キーへ含めると同じRGBでもメーカーや名前の違いで別々になる。一方、キーだけをUIへ出すと利用者が登録した糸名が消える。同HEXの最初の空でない使用糸名を別に保持し、照合と説明の役割を分離した。

Photo Stitchを画面から編集するための適用処理

feature開発中(未リリース)

Photo Stitchを画面から編集するための適用処理

リリース状況

未リリース。Photo制作UIへ接続する非同期処理を実装し、Core502件・App325件の全827件が成功した。通常の作成・再編集画面は後続工程で、サービスの成功と区別する。

何を変えたか

Photoの作成と設定更新をMainActor外で計算し、計算開始時の文書・編集版・選択・レイヤーと現在の状態が一致した場合だけ採用する処理を追加した。設定・解析画像・糸割当を一回のUndoで戻せるCore APIと接続した。

なぜ変えるか

写真の明るさ・密度・糸・解析解像度を変更すると、元画像の復号や針の再生成が必要になる。画面の応答を保ちつつ、処理の途中で選択や文書が変わったときに古い結果が上書きされないようにする必要がある。

技術的な内容

設定、任意の解析画像、糸割当を一回の編集として適用する。設定と糸を別々に変更してUndo回数が増える構成を避け、失敗時は全体を反映しない。

保存済みの解析画像はそのまま再生成に使い、解像度変更を明示したときだけ元PNGを解析し直す。元PNGは下絵と独立した共有資産にあるため、下絵削除後もPhotoの再編集を継続できる。

ユーザーにとって何が良くなるか

一回の適用を一回のUndoで取り消せる。処理を取り消したり、ほかの作品へ移ったりした場合に、古い生成結果で作品が書き換わらないようにする。

判断したこと・学んだこと

非同期処理が正常終了することと、その結果が現在の文書へ採用されることは別である。処理の結果だけでフォームを閉じると、世代照合で拒否した変更でも成功したように見えてしまう。実際の採用を表す通知を分け、後続UIはこの通知を成功遷移の根拠にできるようにした。

試験ではサービス境界で計算結果の返却を一時的に待たせ、取消・選択変更・文書置換の後に解放した。処理結果が届いても、古い結果は採用されず、原本・編集版・Undoが変わらないことを確認した。単に結果をモックするのではなく、実PNGから計算した結果の採用経路を試験した。

検証と未確認範囲

設定のみの更新でRGBAが変わらないこと、下絵削除後も共有PNGから再解析できること、単色/CMYKと糸割当、白/透明/上限超過、生成中の競合拒否を確認。仕様レビュー・独立品質レビューとも残指摘なし。iPadOS26.5 Simulatorで全827件成功、失敗・スキップ・実行時警告0。

途中のSimulator Busyによる起動失敗は元プロセス終了後に専用端末を起動し直し、対象13件を再実行して成功した。通常画面からのPhoto制作・保存再開・出力の操作と、実機ミシンの受入はこの変更の確認範囲に含めない。アプリのローカルコミットはe5a9a7e。

Photo Stitchの元画像と生成設定を作品に保存する

feature開発中(未リリース)

Photo Stitchの元画像と生成設定を作品に保存する

リリース状況

未リリース。生成Coreに続き、元画像・解析入力・配置・設定を編集用文書へ統合した。Photoを作成・再編集する画面の受入は後続工程であり、この記録だけで利用者向けのPhoto Stitch全体が完成したとは扱わない。

何を変えたか

元PNGを共有するPhotoオブジェクトの保存・複製・Undo/Redo、設定と使用糸の編集API、実寸に応じた変形後の再生成、色別のPES/DST/JEF/EXP出力へ接続した。使用する色だけを条件選択や配色変更の対象にし、通常の線編集で写真の矩形だけを書き換えてしまう操作を拒否する。

なぜ変えるか

写真から針を生成しただけでは、後から明るさや密度を変更できない。生成結果を再編集するためには、元画像と生成設定を文書に残し、保存後も同じ入力から針を作れる必要がある。

ユーザーにとって何が良くなるか

Photoオブジェクトを複製したり、制作時の下絵を削除したりしても、再生成に必要な元画像を保持する。通常の画像配置・設定変更と、元画像による再生成を終えて針を直接編集する明示的な変換を区別する。

技術的な内容

文書schema31で元PNGを作品内の共有資産として持ち、各Photoオブジェクトからidで参照する。解析用RGBAは各オブジェクトにDataとして保存する。同じ元PNGを使う複製のたびに画像ファイル全体を重複して符号化しない。

元PNGは解像度再設定などに使う原画像、保存済みRGBAは決定的な生成入力という役割を持つ。読込時にOSの補間処理でRGBAを作り直すと、同じ文書でも生成針が変わる可能性があるため、そのような再サンプルを自動では行わない。

判断したこと・学んだこと

画像量の制限と文書の保存容量は別に検証する。PNGとRGBAがそれぞれ上限内でも、base64表現や他のオブジェクトを含むJSON全体が保存上限を超えることがある。編集の確定前に候補文書の実際の符号化サイズを検証し、失敗で正本やUndo履歴を進めない。

画像の参照切れや壊れたPNGの確認は、通常の文書を開く経路だけでは足りない。復旧下書きから開く経路にも同じ画像検証を適用する必要がある。元画像の完全復号はアプリ境界で行い、Coreは画像量・ヘッダ・寸法・解析入力を検証する。

変形の小さな丸め誤差が、密度の大きな違いになる問題も見つかった。高さ0.8mmを0.4mmで割ると本来2だが、移動後の二点の差が0.8000000000000003になるとceilが3になり、使用色まで変わった。回帰試験で原点0と10の差を再現し、整数の近傍1e-9だけを安定化した。縮小で実際に使われなくなる色がある場合は、外側オブジェクトの先頭色も再生成結果へ同期する。

写真の濃淡をサテン帯へ変換するPhoto Stitch生成Core

feature開発中(未リリース)

写真の濃淡をサテン帯へ変換するPhoto Stitch生成Core

リリース状況

未リリース。Photo Stitchの独立した生成Coreを実装し、仕様・品質レビューとCore試験・Releaseビルドを完了した段階。文書への元画像保存、画面からの生成・再編集、ミシン用書き出しとの接続は後続工程であり、利用者がアプリ内でPhoto Stitchを使える状態にはまだ達していない。

何を変えたか

非乗算RGBA画像から、単色またはCMYKの色別サテン帯を生成するCoreを追加した。帯間隔、暗部の間隔、ガンマ、単色のネガ、使用糸色、アフィン配置を値型として分離した。画像を変更せず、同じ入力から同じ針座標を生成する。

なぜ変えたか

既存の減色・色領域タタミはイラストの色面を作る方式であり、写真の階調をサテン帯の密度で表す機能とは異なる。公式ガイドの公開機能を参考にしつつ、画像処理と生成アルゴリズムを独自に構成する。

ユーザーにとって何が良くなるか

将来、写真を読み込んで刺しゅうへ変換した後にも、明るさや密度を変更して再生成できる基盤になる。今回のCore単体追加によって利用画面が増えたわけではなく、元画像と生成設定を失わない文書統合が次に必要である。

技術的な内容

画像の色成分にガンマ補正をかけ、単色の輝度またはCMYKの色材量を求める。濃度を積算し、閾値に達した位置へ左右一対の針を配置する。色ごと・帯ごとに走査し、空白を挟む針列は独立したrunとして返す。接続側はrun間をジャンプにする責務を持つ。

画像の配置には原点と二本の軸を保存できる型を用いる。スキューした画像で軸の長さだけを帯間隔の基準にすると、帯に垂直な距離と一致しなくなる。行列式と縦軸長から垂直実幅を求め、物理寸法から帯数を決める。

針数だけを求めるモードも、実生成と同じ走査・検証・予算を使う。最大100,000針、前計算を含む最大2,000,000試行を設け、巨大な寸法は整数変換や出力配列確保の前に拒否する。

ハマったこと・学んだこと

一定間隔で画像をサンプルするだけでは、針間隔より細い透明な一行を飛び越え、前後の針をつなぐことがあった。回帰テストでこの失敗を確認し、帯ごとの画素行の濃度とゼロ区間の累積表を前計算する方式へ修正した。前計算も計算量の上限に含める。

行列式が非ゼロであっても、原点が大きく軸が極端に短い場合には、浮動小数点の丸めで左右の針が同じ座標になる。配置と生成対の両方で退化を確認し、針数だけの計算でも同じ入力を拒否する。

判断したこと・制約

帯中心の画素を代表値として扱うため、帯幅内の透明境界をピクセル単位の輪郭として切り抜く方式ではない。解析画像は各辺256画素まで。画面統合時にも帯間隔とサンプル解像度の影響を説明する必要がある。競合と同一のアルゴリズムや写真再現性を主張せず、生地・糸を使う品質受入は最終の試縫いで判断する。

非常に淡い部分は、帯の濃度積算が一対分に達せず針が生じない。例えばRGB254、高さ100mm、暗部間隔0.4mmでは各帯の250サンプルの合計が1未満となる。これは量子化の特性であり、画面では空の結果を成功扱いせず、ガンマや間隔の調整を案内する必要がある。

基準にした刺しゅう図案へ幅と高さを揃える

feature開発中(未リリース)

基準にした刺しゅう図案へ幅と高さを揃える

リリース状況

未リリース。実装・独立レビュー・自動検証は完了。実機・Pencil・ミシン試縫いの受入は最終工程として残す。

何を変えたか

複数の図案から基準を選び、他の図案の幅・高さ・両方を揃える操作を図案管理へ追加した。グループは一組として扱い、基準は変更せず、他の対象の中心位置を維持する。

なぜ変えるか

別々に作った模様を同じ寸法へ合わせる際に、寸法を読み取って対象ごとに入力し直す手間があった。既存の整列は位置を揃える操作であり、寸法を揃える操作とは分けて提供する。

ユーザーにとって何が良くなるか

基準と方式を確認して一括適用できる。文字や曲線、グループ内の位置関係を保ったまま調整し、Undo一回で戻せる。選択を切り替えたりフォームを閉じたりしても基準と方式の入力を保持する。

技術的な判断

選択集合を独立オブジェクトとグループの単位に分け、各単位の輪郭の外接寸法を求める。基準寸法と対象寸法の比率を使い、各対象の中心で変形する。生成針の寸法を基準にすると下縫いや補正の影響を受けるため、編集輪郭の寸法と区別する。

検証と変形はセッションのコピーで行い、全対象が有効な場合だけまとめて反映する。一つの対象が不適合でも他の対象だけ変更することはない。取消や対象切替で古い処理結果を適用しない。

ハマったこと・学んだこと

未適用入力の保存では、利用者が変更した値だけでなく、最初に表示した基準も保存する必要があった。主選択から初期値を毎回計算するだけだと、同じ図案を逆順で選び直した際に基準が変わる。選択ID集合ごとに最初の解決結果を保持し、読み取り用の解決と画面イベントでの初期化を分けた。

同じ寸法に合わせる処理では倍率1の変換を省く。中心から引いて戻すだけでも浮動小数点の丸め差が起き得るため、何も変わらない操作で座標やUndo履歴を変えないようにした。

元輪郭を保ち、領域ごとに縫い方を変える

feature開発中(未リリース)

元輪郭を保ち、領域ごとに縫い方を変える

リリース状況

未リリース。領域別タタミに、二レールRow Fillと列サテンを追加した。App/Coreの全733件、混合方式を保存してアプリ再起動後に再編集するSimulator試験、実機向けRelease署名なしビルドが成功した。実機縫製の受入は未実施。

何を変えたか

分割した領域に縫い方とその設定を保存するモデルを追加した。輪郭や分割線を編集しても、保存した端辺の指定からレールを再生成する。領域を分割した場合は子をタタミで開始し、親に保存した縫い方は分割を削除すると復元する。

なぜ変えたか

同じ図形の中でも、面を埋めたい箇所と線に沿わせたい箇所では適した縫い方が異なる。領域を別々のオブジェクトへ分解すると、後から分割線を動かしたり分割前へ戻したりする操作が難しくなる。

ユーザーにとって何が良くなるか

元の図案を作り直さず、領域ごとに縫い方や間隔を調整できる。正本と生成した針を分けることで、編集用文書を保存した後も再生成できる。

技術的な内容と判断

  • 二つの端辺は四つの境界anchorで表す。生成後の辺番号を保存せず、元の辺や曲線のIDとパラメータを持つ。
  • 各方式の処理量と針数は分割オブジェクト全体で合算する。領域ごとに上限をリセットしない。
  • 非稼働の親領域にも設定を保持する一方、品質警告は現在縫われる葉と縫い方式だけを調べる。

ハマったこと・学んだこと

直線かどうかを座標の外積が厳密にゼロになることで判定すると、回転後の丸め誤差で直線を拒否する場合がある。一方、許容幅を大きくしすぎると実際の曲線を直線として受理してしまう。元のsourceが直線かという情報と、座標精度による丸め誤差の範囲を分ける必要があった。

プレビュー用の公開APIと文書検証が異なる情報を持つと、表示では成立しているのに適用に失敗する。派生した境界にも元sourceの直線性を付け、同じ判定へ接続した。

端辺が複数の派生辺に分かれた場合、フォームの番号だけで再構成すると意味が変わる。片方の入力変更で反対側の保存指定まで失わないよう、左右の未適用入力とcanonicalな端辺を個別に扱う。

検証範囲

App286件とCore447件が成功し、失敗・スキップ・実行時警告は0。専用の追加試験はCore19件、App9件。Tatami/Row/Satinを含む3領域について、保存往復、オブジェクト両端だけの留め縫い、PES/DST/JEFの独立readerによる座標と色表現、針変換とUndoを検証した。仕様レビューの2指摘を修正後、独立品質レビューで修正必須の指摘はなかった。

Simulatorで方式・端辺・間隔を編集し、草稿の閉じ再開、Undo/Redo、保存・プロセス終了・再起動後の設定保持を確認した。最低対応iPadOS17の実行と物理iPad・Pencil・ミシンは未確認であり、Simulatorの成功を実機受入に読み替えていない。

起動・新規作成・案内開閉・準備スキップ・保存文書の再起動後再開も、最終コードでiPadOS26.5/27の各4件、計8件が成功した。追加の起動不具合は検出されなかった。

分割線を編集し直せる領域別タタミを開発

feature開発中(未リリース)

分割線を編集し直せる領域別タタミを開発

リリース状況

未リリース。元輪郭、分割線、領域設定の保存モデルと生成・編集API、編集フォームを追加した。独立レビューの指摘を修正し、App277件・Core428件の計705件、対象UI1件、署名なしgeneric iOS Releaseビルドが成功。物理iPadやミシンでの最終受入は残る。

何を変えたか

一つの面を直線または三次曲線で分け、領域ごとにタタミの角度・間隔・名前を持たせる機能を追加した。元輪郭と分割線を保存し、生成済みの小さな面へ置き換えるだけの編集にしない。分割を削除すると、保存しておいた親領域の設定へ戻す。

なぜ変えたか

図形演算による結合や差分だけでは、後から分割線を動かして縫い方向を調整する操作を再現できない。複数の縫い方向を持つ図案を、制作の途中でも編集できる基盤が必要だった。

ユーザーにとって何が良くなったか

輪郭を作り直さず、領域の分け方と縫い方向を調整できる。親の分割線を動かした結果、子の分割が成立しなくなる場合は変更全体を拒否し、設定が別の領域へ黙って移動しない設計とした。

技術的な内容と判断

  • schema29で、UUIDを持つノードと分割線を平坦な配列として保存する。循環や祖先以外への参照は検証で拒否する。
  • 分割線の端は生成後の辺番号ではなく、元の辺または祖先の曲線のIDとパラメータで表す。
  • 子の分割線が参照するパラメータを、親曲線の細分化へ必ず含める。曲線の近似点数が変わっても、親子の境界点を共有できる。
  • 独立した領域の針列を生成し、作業量・頂点・針数の予算を全領域で共有する。領域ごとに上限をリセットしない。
  • 非同期の再実行は、元の対象ID・編集版・入力世代が一致する場合だけ受け付ける。

ハマったこと・学んだこと

生成された多角形の辺番号を保存すると、親曲線の細分化が変わっただけで子の端点が違う場所を指す。見た目の座標だけではなく、その座標がどの正本に由来するかを保存する必要がある。

非同期処理の成功時に世代を照合していても、「再実行」が古い操作と新しい選択対象を組み合わせると、別のオブジェクトを編集できてしまう。再実行する操作そのものにも対象と世代を結び付けた。

分割線のフォームには、もう一つ境界の由来が必要だった。元の正本がsource+tを保持していても、入力途中の草稿が派生辺番号だけを持つと、同じ適用を繰り返すだけで端点が移動する。境界snapshotから再写像し、点挿入・反転など意味が変わる場合は文字列を残して明示的な再指定を求めるよう修正した。独立レビューで検出し、反復適用と親曲線変更の回帰テストを追加した。

全体試験は、種類の一致を調べず残存する別種類の設定まで品質警告に使う回帰も検出した。既存試験を変更せずkind一致条件を修正し、最終試験をやり直した。

刺しゅう図案を縦横別の寸法と任意の中心で変形する

feature開発中(未リリース)

刺しゅう図案を縦横別の寸法と任意の中心で変形する

リリース状況

未リリース。今回の実装・独立レビュー・自動検証は完了した。実機での受入とミシンの試縫いは最終工程として残している。

何を変えたか

既存の数値変形に、縦横別の倍率、幅・高さの指定、任意の中心座標、横または縦のスキューを追加した。輪郭だけでなく、文字の基底、曲線の制御点、左右レール、分割領域、クロスステッチの格子、取込針の命令を、それぞれの編集可能な形で変形する。

なぜ変えたか

等比拡縮だけでは、決まった幅へ細長く合わせたり、図案の外側を中心に回転したりする調整ができない。既存の配置操作の中で、寸法と基準点を明示して操作できるようにする。

ユーザーにとって何が良くなるか

元の図案を作り直さずに、幅と高さを別々に調整できる。文字や曲線も変形後に内容や制御点を編集し続けられる。

技術的な判断

編集輪郭の外接寸法と、引き補正・下縫いを含む生成後の針の寸法を区別する。指定寸法は毎回最新の輪郭から倍率へ変換し、過去の入力欄の寸法を計算元にしない。0幅の縦線を幅指定で拡げることはできないが、高さだけの変更は可能とする。

新しい数値操作は編集セッションのコピーで非同期に検証・変形する。選択集合、編集版、入力の世代が開始時と一致する場合だけ適用し、取消や対象切替で古い結果が別の図案へ入るのを防ぐ。

ハマったこと・学んだこと

入力草稿をViewだけに持たせると、単一選択と複数選択でフォームが作り直される際に入力が消える。編集モデル側に選択IDの集合をキーとして保持し、対象の選択順序やフォームの再生成に依存しないようにした。

Simulatorでは親DisclosureGroupのアクセシビリティ識別子が子のスイッチ取得に影響した。親は表示ラベル、入力と適用操作はそれぞれの識別子で特定することで実操作テストを通した。文字の再描画は座標の演算順による微小差が出るため、点数と順序は厳密に、座標は1e-10以内の差で一致を確認した。

左右の輪郭に沿うRow Fillと境界を保つ変換

feature開発中(未リリース)

左右の輪郭に沿うRow Fillと境界を保つ変換

何を変えたか

左右の輪郭を編集して縦方向の縫い目の流れを変えるRow Fillを追加した。閉図形の端となる二辺を指定する入口と、列サテンの左右輪郭を利用する入口を設けた。開発中・未リリース。

なぜ変えたか

輪郭と中心点による周回・螺旋だけでなく、湾曲した帯などで形に沿う縦の列を作れるようにするため。Janomeの公式ガイドを機能の参考にし、針生成と編集方法は独自に設計する。競合製品の素材・画面・コードは使用しない。

ユーザーにとっての改善

生成した針の集合へ平坦化せず、左右の輪郭を編集正本として残す。輪郭の点を動かすことで再生成でき、通常の移動・拡縮、保存再編集、Undo/Redoへ接続する。閉図形の変換前には端A/Bをプレビューして向きを選べる設計とした。

技術的な判断

閉輪郭上の二つの頂点を共通の端点にすると、全ての縦列が同じ点に集まり、端の幅もゼロになる。そこで二本の辺を端に選び、残りの境界を左右の鎖に分ける。端の辺も含めて元の境界を維持でき、列の端点が一か所へ必ず収束する形を避けられる。

左右の鎖の点数が異なる場合は、それぞれの弧長に対する頂点の位置を合わせる。片側の点数に合わせて他方を粗く採り直すと、元の角が消える。両方の頂点位置を合わせた分割を使い、元頂点を保持して他方だけを補間する。

学び・トレードオフ

左右の対応線同士が交差しないだけでは、凹形状の外側を通る対応を十分に除外できない。対応線と外周の交差、隣接する対応線で囲む小領域の凸性、全領域の向きが揃うことも検証条件に含める。

一直線の補助点を許す弱凸判定では、正常な形の回転だけで外積が微小な負値になるケースを回帰テストで発見した。外周・交差は正常でも、符号の厳密比較で編集を拒否していた。固定の大きな許容値で全ての検査を緩めず、弱凸の外積比較だけに座標の浮動小数点精度から求めた丸め幅を適用する方針とした。

UI試験では、画面内にあるCollectionViewを選ぶだけではキーボードの候補欄まで拾ってしまった。常設フォームは非表示のものも残るため、詳細モーダルのアクセシビリティ要素を起点に対象を限定する。表示範囲外のList行は、待つだけでは要素として現れず、実際にフォームをスクロールする必要があった。

列数は最も幅の広い対応線から決めるため、狭いところの間隔は小さくなる。間隔設定や針落ち位置の交互ずらしを、局所的な密度の均一化と同じ機能とは扱わない。複雑な方向線、領域分割、短長処理による密度調整は別の実装課題として保持する。

検証結果

App268件・Core406件の計674件と、iPadOS26.5 Simulatorの対象UI2件が成功した。失敗・スキップ・実行時警告は0。実機向けRelease署名なしビルドも成功。矩形の端辺選択、間隔0.50mm、針落ちずらし、右側の点編集、Undo/Redo、保存してアプリを終了・再起動した後の440針と設定を確認した。64辺の円は端辺A/Bのプレビューを別のUIテストで確認した。全UIケースや全対応OSの合格とはしない。

実装・試験結果と8組の画像/AXは開発リポジトリの docs/design/qa/2026-10-07-row-fill-rails/QA.md で管理する。PES/DST/JEFは独立decoderで座標とジャンプを照合した。アプリ内のお知らせ機能は見つからず、未リリースのためリリース告知は行っていない。

閉輪郭と中心点を保持するRow Fill

feature開発中(未リリース)

閉輪郭と中心点を保持するRow Fill

何を変えたか

閉じた図形の輪郭に沿って内側へ周回する縫い方と、指定した中心まで連続して進む螺旋を独立実装した。中心位置・間隔・針目長・針落ち位置の交互ずらしを調整し、元輪郭と中心の設定を編集用作品へ保存する。開発版への追加であり、未リリース。

なぜ変えたか

平行な列で埋めるタタミや二方向の網目とは異なる、輪郭と中心に沿う流れを作れるようにするため。公式資料を図も含めて確認し、点方向のRow Fillと、中心へ放射状に縫うSatinを区別した。

ユーザーにとって何が良くなったか

閉図形から周回・螺旋を選び、中心をずらして流れを変えられる。針データへ不可逆に置き換えることなく、元輪郭の点編集、移動・回転・拡縮、Undo/Redo、保存再編集を継続できる。

技術的な内容

輪郭の各頂点を中心へ相似縮小する。指定中心からすべての境界辺を見通せることを、辺の内側半平面の共通領域で検証する。共通領域内なら各辺と中心の三角形内に針経路を保てる。初期中心もこの領域から選び、単に外接矩形の中心を使わない。

周回はそれぞれ閉じた走行、螺旋は輪郭周長に沿う進行と縮小率を連動させた一続きの走行にする。角を残してから針目長以下へ分割する。編集正本と生成針を分離し、中心も輪郭と同じ座標変換を受ける。保存形式は27へ更新し、旧形式に新設定が混入したデータは拒否する。

判断したこと・トレードオフ

等距離オフセットの繰り返しでは中心変更を表現できないため採用しなかった。任意の凹形状や穴を無条件に扱わず、対応できない場合は編集前の作品を保持して理由を表示する。対応中心を持つ一部の凹形状は扱える。

間隔は対応する半径方向の最大間隔で、局所的な垂直間隔の均一化ではない。針落ち位置の交互ずらしも、競合製品の短長機能全体と同等とは扱わない。

ハマったこと・学んだこと

最大針目長から微小量を引いた固定歩幅で辺を刻むと、辺長が針目長の整数倍のときに、終点直前へ極端に短い針を作る。最大長を守るつもりの丸め対策が、刺しゅうでは余計な針落ちを増やしてしまう。回帰テストを追加し、分割数を求めて等分する方式へ修正した。

中心線を保った幅指定の輪郭サテン

feature開発中(未リリース)

中心線を保った幅指定の輪郭サテン

何を変えたか

線や図形の中心線を正本にし、幅・間隔・補正・ずらし・センター下縫いを指定する輪郭サテンを追加した。既存の左右レール式とは別の縫い方として扱う。未リリース。

なぜ変えたか

単純な輪郭でも左右のレールを利用者が個別に作る必要があり、元の曲線や文字を保った幅変更ができなかった。中心線と縫い設定を分けることで、元の形を再編集しながら縫い幅を調整できるようにした。

ユーザーにとって何が良くなったか

線や閉じた図形を幅付きの縫い方へ切り替え、同じ図案上で輪郭と針の設定を編集する。文字列・曲線の正本も維持し、保存・Undo/Redoへ接続した。

技術的な内容

辺の接線と法線から派生レールを生成し、角では隣接法線の交点方向を使う。閉路の最後の辺も生成対象に含める。帯が自己交差する太さや鋭すぎる角では、元の形を勝手に変えず編集を拒否する。

ハマったこと・学んだこと

幅を持つ線は、中心線が正しくても内側のレールが折り返す場合がある。正本の妥当性に加え、生成する帯の交差・対応点・継ぎ目を検証する必要がある。単純な幅の数値チェックだけでは足りない。

下縫いを上縫いの細分点から作ると、上縫い間隔を細かくしたとき下縫いも過密になる。派生レールの頂点中点から下縫いを独立生成し、上縫いの間隔変更で下縫い針列が変わらない回帰試験を追加した。

右側パネルを廃止し、上部リボンと対象別の詳細モーダルへ統合

feature開発中(未リリース)

右側パネルを廃止し、上部リボンと対象別の詳細モーダルへ統合

何を変えたか

常設・重なり表示の右側操作パネルを廃止した。分類タブは上部の道具の表示だけを切り替え、入力が必要な操作から中央の詳細モーダルを開く。キャンバスは縦横とも全幅を使う。未リリース。

なぜ変えたか

上部の操作と右側の一覧が似ており、操作場所が二重になっていた。ユーザーから、Wordのように上部へ操作をまとめ、詳細だけをモーダルで表示する指示を受けた。

ユーザーにとって何が良くなったか

上部から図形をすぐ追加し、図案の選択・複製・削除、針順の再生を行える。文字、下絵、曲線、枠、糸ライブラリ、入力方法などは対象ごとの詳細を開く。設定一覧を開くためだけに分類タブを押す構成をやめ、道具とキャンバスの関係を分かりやすくした。

技術的な内容

SwiftUIの上部リボンで、タブ選択と詳細表示のコールバックを分離した。中央モーダル内のフォームは常設し、閉じても未適用の入力を保持する。非表示フォームはタップ・アクセシビリティから除外し画面外へ移す。背面をブロックし、モーダルの見出しへVoiceOverのフォーカスを接続した。素材追加成功時は閉じ、処理中・失敗時は維持する。正本、針生成、保存、Undo処理は既存モデルを使う。

判断したこと・トレードオフ

詳細を入力している間は制作画面への操作を止める。モーダルを閉じる操作が必要になるが、長い右側設定一覧がキャンバスを狭める問題を避ける。DESIGN.mdに右側パネル禁止、上部入口、対象別詳細、入力保持を記録した。

ハマったこと・学んだこと

入力保持のために常設した非表示フォームは、XCTestのexistsだけでは検出される場合がある。テストでは実際にタップできる状態を区別し、古い右側UIでは新しい回帰テストが失敗することを確認した。文字更新の確認は別図案を選んでから確定済み正本を読み直し、未適用入力だけで誤合格しないようにした。モーダルのAX枠は非表示フォームを含むため、中央配置は画面内で操作可能な入力フォームの実際の枠で確認する。不正寸法入力はカーソル末尾の座標に頼らず、標準の全選択で値を置き換える。さらに、行の見た目とボタンのタップ領域が一致しない問題を共通部品で修正し、行の右側余白を実タップする回帰確認を追加した。

確認結果

初期統合版はUI10件の一括実行が成功した。最終版はSimulatorのアニメーション待機で中断して分割実行へ切り替え、10ケースそれぞれの成功を確認。不正寸法の入力操作と中央位置の測定方法を修正し、該当ケースを個別再検証した。最終版の一括実行が全件成功したとは扱わない。実画面で全幅キャンバスと中央モーダルを確認し、QAに画像・結果・未確認事項を記録した。最終ソースのApp 208件・Core 308件、合計516件は一括実行で失敗0件。PES実ファイル1312 bytes、ヘッダーとPEC参照位置も確認。

元の輪郭を保つ網目の内外調整と固定間隔

feature開発中(未リリース)

元の輪郭を保つ網目の内外調整と固定間隔

何を変えたか

未リリースの開発版で、網目の縫う範囲を内外へ一定距離調整する設定と、線の間隔を固定する設定を追加した。元の面や文字の正本を変更せずに生成針だけを調整する。

なぜ変えたか

網目を作るだけでなく、穴の周囲へ少し重ねる、縫う範囲を内側へ退かせる、網目の間隔を明示する操作が必要だった。図形を拡大縮小する方法では、辺からの距離が一定にならず、穴の形も目的どおりに変わらない。

ユーザーにとって何が良くなったか

内外調整を戻しても元の輪郭を描き直す必要がない。固定間隔は通常の面設定と分け、旧作品を開いたときの針配置を保つ。設定は保存とUndo/Redoの対象になる。

技術的な内容

輪郭を正規化した後、境界に丸い帯を作り、外側へ広げる場合は領域との和、内側へ狭める場合は差を取る。文字が重なる場合も、内部の重複境界を取り除いてから帯を作る。穴が閉じる、細い部分が分離する形状でも、編集正本と生成領域を分けて扱う。

固定間隔は線の距離を保ち、領域端の余白を両側に配分する。斜めの網目では菱形の一辺と線間距離が異なるため、表示と説明も区別する。

ハマったこと・学んだこと

新しい生成パラメータを追加すると、針生成だけでなく品質チェックも使用中の値へ追従させる必要がある。固定間隔があるのに元の面間隔だけで過密を判定すると、誤った注意や見逃しが起きる。また、枠に合わせる際は元輪郭だけでなく調整後の針の張出しを確認する必要がある。

編集輪郭を保持する二方向の網目縫い

feature開発中(未リリース)

編集輪郭を保持する二方向の網目縫い

何を変えたか

未リリースの開発版で、面オブジェクトへ二方向の網目縫いを追加した。元の輪郭と穴、文字の編集情報を正本へ残し、生成された針とは分ける。基準角度と交差角度、行間隔、最大針目長を使って再生成する。

なぜ変えたか

刺繍制作をこのアプリだけで完結させるため、タタミだけでなく網目状の縫い方が必要だった。二つの独立したオブジェクトに複製すると、穴や文字の再編集時に片方だけ変わる恐れがあるため、同じ面の生成設定とした。

ユーザーにとって何が良くなったか

面の詳細で網目縫いへ切り替え、交差角度を調整できる。編集用文書に設定を保存するため、針データへの平坦化をせず再調整できる。

技術的な内容

既存の穴付き面の走査処理を二方向へ適用する。下縫いは最初の一回に限り、方向を変える箇所は別のrunとして保つ。二回目の生成でも針数と走査処理の上限を共有する。個々のパスだけが上限以内でも、合計が上限を超えるデータは拒否する。

網目の設定は任意フィールドとし、旧文書では従来のタタミ生成を維持する。schema24で新しい設定を保存し、古いschemaに新設定を紛れ込ませた文書は拒否する。

ハマったこと・学んだこと

機能比較表にNet Fillという名前があっても、対応する調整操作が分からなければ同等とは言えない。公式の詳細マニュアルを照合し、内外オフセットと大きなセル寸法の操作が追加で必要と分かった。独自の二方向生成を実装したことと、競合機能の網羅は区別して管理する。

iPadOS26.5で新規作品を開けない起動経路を修正

fix開発中(未リリース)

iPadOS26.5で新規作品を開けない起動経路を修正

何を変えたか

未リリースの開発版で、新しい作品の作成ボタンをDocumentGroupの既定ファクトリにつながる標準NewDocumentButtonへ変更した。型付きテンプレート初期化を空の文書作成に使わない構成とした。

なぜ変えたか

iPadOS27では成功していた新規作成が26.5では準備画面へ進まなかった。ボタンは押せており、OSの文書作成要求も記録されたが、SwiftUI側で文書をシリアライズできず作成結果URLがnullになっていた。

ユーザーにとって何が良くなったか

26.5でも起動画面から準備と制作へ進める。編集用.stitchの形式、既存の保存先、編集モデル、文書管理は維持する。

技術的な内容

変更前後を同じOSで比較した。失敗時のログは Content serialization failed, document won't be saved. とSwiftUI文書エラー101。ボタンの初期化だけを変更すると作成結果URLが返り、準備画面へ進めた。Info.plistの不足や初回起動の待機不足を原因と決めつけず、UIタップ・文書作成要求・シリアライズ結果の境界を追った。

これは本アプリの構成での検証結果であり、すべての型付きNewDocumentButtonが失敗するという意味ではない。Apple Developer Forumsの類似報告も参考にしたが、修正の根拠は実際の再現と変更後の回帰確認である。

ハマったこと・学んだこと

  • 起動画面が変わらない現象でも、タップ不達と保存失敗は別問題。OSの文書管理ログで区別できる。
  • 保存再開を同じプロセスだけで確認せず、アプリ終了・再起動後に一覧から開くUIテストへ強化した。
  • 最新OSだけのテストでは、旧OS側に残る作成経路の違いを見落とす。専用Simulatorを作るスクリプトに --startup-only を追加し、同じ4件をOS別に確認できるようにした。

作成済みオブジェクトの直接編集と素材の拡充

feature開発中(未リリース)

作成済みオブジェクトの直接編集と素材の拡充

何を変えたか

未リリースの開発版で、キャンバス上のオブジェクトをタップして選び直し、ドラッグで移動、角ハンドルやピンチで等比拡縮する機能を実装した。対象外の二本指操作はキャンバスの表示移動・倍率に使う。点編集とは明示的に切り替える。

未命名の対象は「オブジェクト1」「オブジェクト2」と表示し、一覧をレイヤー設定より前に置いた。選択対象の名前を一覧や詳細から変更できる。既に付けた任意名は保持する。

四角・楕円・角丸四角・三角形・ひし形・五角形・六角形・星・ハートの9図形と、直線・折れ線・ジグザグ・円弧・波線の5プリセットを追加した。円弧と波線はBezier制御点を保持し、閉じた図形は面縫いにも変更できる輪郭で保存する。

内蔵フォントを28書体追加し、既存Noto Sans JPと合わせて29書体(日本語23・欧文6)にした。名前・分類で検索でき、各書体のライセンスをアプリ内で表示する。利用者がフォントを取り込む機能は追加していない。iPadOS27 Simulatorで統合UI検証を完了した。

なぜ変えたか

既存の画面にはリボンの一覧から再選択する機能があったが、キャンバスには描画と表示操作しかなく、利用者が作成済みの対象を直接つかんで編集できなかった。

ユーザーにとって何が良くなったか

対象を一覧から探す操作を減らし、位置と大きさをキャンバス上で調整できる。数値編集も代替操作として残す。名前を付けることで、複数の対象を後から見分けやすくなる。

技術的な内容

操作中は編集正本を確定せず、一時的な変形結果を描画する。指を離した時に既存の変形処理へ一回だけ渡すことで、Undo一回で操作前へ戻せる。文字の輪郭だけでなく、文字ソース、Bezier制御点、サテンのレールなどの編集可能な情報を保持する。

開始時の文書revision・選択・入力世代・表示領域を記録し、途中で対象が変わった操作を拒否する。ロック・非表示のメンバーを含むグループを部分的に変形しない。

ハマったこと・学んだこと

  • 1本指ドラッグの取消通知が、新しいピンチ開始後に届くイベント列をテストすると、共有していた状態が消えて表示倍率の操作へ切り替わった。recognizerごとのcontextに分離し、取消対象のIDを照合した。

  • サテンの編集正本は左右の開いたレールである。線の近傍だけを当たり判定すると見えている内側が選べないため、レールをつないだ領域も判定対象にした。

  • XCUITestの座標を選択枠に相対的な形で保持すると、次の選択枠へ遅延解決される。観測した位置をWindow基準へ変換して、元のオブジェクトをタップするテストにした。

  • UIテストの終了後にSimulatorの診断収集が停止した。未完成の結果束を成功証拠にせず、ログと未実行範囲を分けて記録した。

  • 常設フォームのonAppearは、そのフォームが利用者に見える前に呼ばれる。初回準備で枠を縮めた際に古い中心位置が残らないよう、未入力値だけを現在の枠から解決し、明示入力した座標を保持する方式へ変更した。

  • 正多角形や星を単に幅・高さの半径で生成すると、実際の外接矩形が指定寸法にならない。輪郭の実測範囲を指定矩形へ写像して揃えた。

  • 文字フォームの未適用入力を対象別に保持すると、再選択で入力欄に同じ文字が出るだけでは、更新操作が正本へ反映された証拠にならない。UIテストに更新のUndo・Redoとオブジェクト数の確認を加えた。

  • 保存正本の位置や書体が変わった場合は、その項目を下書きへ反映する。他の未適用項目は保持することで、文字を入力途中に移動・Undoしても古い位置で上書きしない。

  • 文字フォームのSectionに付けていた複数sheetを、フォームが所有するListの単一sheetへ集約すると、実画面で書体一覧・検索・選択が開くようになった。常設フォームと対象別下書きは維持した。

  • iPadOS27の浮動数字キーボードが標準の全選択メニューの位置を覆い、メニューをタップしたつもりで0が入力された。操作前後の実画面とAXを照合し、準備の幅・高さだけ下部キーボードを使う入力種別へ変更。数値置換と小枠での初回配置を確認した。

  • 統計計算を取消したのに進行中表示が残り、同じデータで画面を開き直しても再開しない分岐を修正した。

  • 文字更新テストでは、入力後にアニメーション完了通知を待ち続ける現象も発生した。統計の反復実行は観測されず、文字の自動補正・大文字化を無効にした同一ケースでは更新・Undo・Redoまで成功した。入力どおりの文字を保持する設定だが、OS内部の通知不達の原因すべてを確定したわけではない。

刺しゅうの配色案を正本と分離して扱う

feature開発中(未リリース)

刺しゅうの配色案を正本と分離して扱う

何を変えたか

テーマ配色と色相調和を使い、選択した刺しゅう図案の色を一括変更する処理を追加した。鮮やか・パステル・アースと8方式の色相調和を独自のHSV計算で生成する。未リリースの開発版。

なぜ変えたか

通常の輪郭だけでなく、クロスステッチはパレット、取り込んだ針データは色ブロックに色を持つ。単純にオブジェクトのRGBだけを変更すると、一部の表示が変わらず、古いメーカー・糸番号が残る問題があるため。

ユーザーにとって何が良くなったか

同じ元色を同じ提案色へ対応させて複数の図案を変更できる。文書内の登録糸へ近似する場合は、色と糸番号を一緒にコピーする。近似しない任意色に以前のメーカー番号を流用しない。

技術的な内容

配色案は元の文書と変更後の文書を持つ不変のスナップショットとし、生成だけでは正本を更新しない。確定時だけ一回の編集として適用し、Undo/Redoへ接続する。形状・針位置・停止・色替え・糸切り・縫い順は色の変更から切り離して保持する。

ハマったこと・学んだこと

文書の値が同じかの比較だけでは、別の図案を選んで元へ戻った操作や、編集後にUndoした操作を区別できない。UI側でも選択の世代と編集revisionを照合し、以前の配色案を誤って適用しないようにする必要がある。

一方、同じ配色をもう一度適用する操作は正常な変更なしである。revisionの増加だけを成功判定にすると、正常終了しても画面が閉じなくなる。適用処理の成功とデータの変化を分け、余分なUndoを作らずに閉じる回帰試験を追加した。

登録糸の近似で複数の色が一つにまとまっても、取込データの明示的な色替え命令を消してよいとは限らない。通常図案の自動生成される色替えは新色から再計算し、取込針の明示命令は保持する。色の置換と縫い順の最適化を分け、既存の形式別出力制約も維持する。

制作導線をMac上で操作するUI自動テストを追加

feature開発中(未リリース)

制作導線をMac上で操作するUI自動テストを追加

何を変えたか

XCUITest専用ターゲットと実行スクリプトを追加した。新規作成、枠の準備、図形追加、文字入力保持、Undo/Redo、針送り、保存再開、書き出しを実際のSwiftUIと標準文書UIから操作する8ケースを作成した。未リリースの開発用検証基盤である。

なぜ変えたか

全機能を手作業で毎回確認する負担を減らし、UIの追加や整理で制作の主経路が壊れることを検出するため。

ユーザーにとって何が良くなったか

Mac上で制作の主経路を繰り返し確認できる。各実行に新しい専用iPad Simulatorを作り、普段の作品からテスト用文書を分離する。

技術的な内容

Swift 6 / XCUITest / XcodeGen / simctlを使用。通常の起動Scene・DocumentGroupを操作し、モデルへ直接データを注入する専用起動経路は設けない。PESは保存完了の表示だけでなく、実ファイルのヘッダーとPEC参照位置もスクリプトで確認する。終了時に削除するのは実行が作ったSimulatorだけ。スクリーンショットとAXツリーをxcresultへ記録する。

判断したこと・トレードオフ

単体テストとUIテストは別スキームにし、生成アルゴリズムの詳細検証と制作導線の操作検証を分けた。テストは8ケースから始め、取込や復旧などの分岐は今後追加する。

ハマったこと・学んだこと

シミュレーターでは、可視かつ有効なSwiftUI操作にisHittable=falseが返る現象を観測した。画面内の四角追加ボタンの観測した枠へ実際にタップすると33針が生成されることを確認した。通常タップを優先し、代替はWindow内で候補の枠が一意な場合だけ、その枠の中央への実タップを許す。新規作成には同名の非表示AX操作もあるため、主操作を区別し、isHittableになるまで待つ。この操作では代替タップを使わない。「ブラウズ」を押すと標準一覧が全画面へ広がり、起動Sceneのボタンが隠れる一方でAX情報は残る。存在と枠だけで起動操作を判断できないことを、失敗画面とAXツリーの比較から確認した。固定の画面位置やモデルへの直接操作で成功扱いにしない。

この代替操作はAXのヒット判定やVoiceOverの正常性を保証しない。OSごとの実行結果を記録し、制約を残す。

同じ名前の入力欄が非表示の設定パネルにも残るため、最初のAX要素ではなく操作可能な入力欄を選ぶ。右寄せの数値欄は値の末尾をタップして削除し、入力後の値自体も検証する。文書管理のLabeledContentは「作品名」と値をまとめて読み上げるため、保存再開の文書名は標準ナビゲーションバーから取得し、一覧の同名項目を選ぶ。製品コードへのテスト専用変更は不要になった。

確認結果

2026-10-05、Mac / iPad A16 / iPadOS 27 Simulatorで新しい専用デバイスを作成し、全8件が失敗0件で成功した(テスト実行500秒)。実行スクリプトも終了コード0で完了し、専用デバイスの削除まで確認した。標準保存で出力したPESは1312 bytes、ヘッダー#PES0060、PEC参照位置127のLA:を確認し、実ファイルと検証記録を保存した。最終ソースで既存App 208件・Core 308件、合計516件も成功。物理iPadやミシンでの合格記録ではない。

テストコードと実行スクリプトはリポジトリに保存してあり、同じコマンドで回帰確認を繰り返せる。定期ジョブやmacOS CIへ接続できるが、定期実行のスケジュールはまだ設定していない。

道具の入口を上部リボンへ集約し、作成済み図案の再編集を改善

feature開発中(未リリース)

道具の入口を上部リボンへ集約し、作成済み図案の再編集を改善

何を変えたか

編集画面の上部に共通操作、「挿入・図案・編集・確認・設定」のタブ、分類別の道具を表示するリボンを追加した。従来の「ツール」メニューと左側の分類切替を置き換え、詳細入力は右側へ配置した。未リリース。

なぜ変えたか

作成済みの対象を選び直す入口が分かりにくく、主要な道具がメニュー内に隠れていた。Wordのように画面上部から制作の操作を発見できる構成をユーザーが求めたため。

ユーザーにとって何が良くなったか

詳細パネルを開いても上部の分類を切り替えられる。「図案を選択」で作成済みの対象を選び直し、そのまま編集できる。文字は上部の「文字を編集」から内容を更新する。作品管理・ガイド・ミシン用出力も上部のラベル付き操作から開ける。

技術的な内容

SwiftUIのEditorRibbonへ入口と操作の表示を分離した。既存のEditorViewModel、保存・生成・Undo処理、ZStackで常設する5つのフォームを維持し、未適用入力を分類変更で破棄しない。狭い幅ではキャンバスだけをブロックして右側の詳細を重ね、リボンは利用可能にする。横幅が足りない行は横スクロール、アクセシビリティ文字サイズでは上部領域を縦スクロールできるようにする。

判断したこと・トレードオフ

全設定をリボンへ展開するとキャンバスが狭くなるため、上部は入口と基本操作、詳細は右側へ分けた。新機能の入口は既存タブへ追加する規則をDESIGN.mdに記録し、共通操作やタブを機能ごとに増やさない。

ハマったこと・学んだこと

常設フォームの入力欄を読むだけでは、更新ボタンが何もしなくても未適用入力を読んでテストが合格する。文字更新テストでは更新完了後に別図案を選び、対象を再選択して確定済みの文字を読み直すようにした。旧UIで上部タブがない失敗を先に確認し、その後に実装した。

確認結果

最終コードで既存App 208件・Core 308件、合計516件が成功した。上部配置・図案再選択・既存文字更新を含むUIテスト全9件も失敗0件(609.599秒)で成功した。実行スクリプトは終了コード0。実保存PESは1312 bytes、ヘッダー#PES0060、PEC参照位置127を確認した。回転後の入力欄の配置・操作可能状態の待機を加えたケースも個別再実行して成功した。Device Hubで縦横の実表示と横画面の編集タブ切替を確認した。

初めての作品づくりを案内するUIへ再構築

feature開発中(未リリース)

初めての作品づくりを案内するUIへ再構築

何を変えたか

起動に「新しい作品を作る」を表示し、任意の枠準備と、図形・文字・下絵・手描きから始める入口を追加。編集を追加・図案・編集・確認・作品設定の5分類へ整理し、準備→素材→編集→確認→保存の制作ガイドを設けた。編集文書・復旧下書き・ミシン用出力を分けて説明し、出力前に形式・寸法・針数・糸順・品質を確認する。未リリース。iPad専用のまま。

なぜ変えたか

最初の新規作成が分かりにくく、機能が長い設定一覧へ並び、次の操作を探す必要があったため。

ユーザーにとって何が良くなったか

基本操作が先に見え、詳細は必要時に開ける。ガイドから実操作へ進める。図形・文字ならPencilなしでも始められる。保存と機械用出力の違いが分かる。

技術的な内容

DocumentGroup、文書同期、編集モデル、針生成を維持。iPadOS18以降はDocumentGroupLaunchSceneとNewDocumentButtonを使い、17は日本語化した標準ブラウザを維持。UISupportsDocumentBrowserを設定したが、これだけが作成エラーの原因だったとは断定しない。

5パネルはZStackで常設し表示・タップ・アクセシビリティを切り替える。文字・下絵・曲線のフォームは一箇所に集約し、分類変更で未適用の入力値を保つ。出力形式はsheet(item:)で渡す。文字拡大時は上部をスクロール可能にしてキャンバスを残す。

共通トークンと操作カードを追加。docs/design/DESIGN.mdに配置台帳、思想、追加機能のルールを記録し、AGENTS.mdにも参照規則を追加した。

Before / After

Before: 空の編集画面から長い設定一覧を探して図形を追加する。 After: 新規作品→任意の枠設定→図形→四角を追加で開始できる。

トレードオフ

新しい起動Sceneは18以降に限定し、17の最低対応を維持。標準文書管理によるOSごとの外観差を許容する。常設の非表示パネルは操作とVoiceOverを無効化する必要がある。

ハマったこと・学んだこと

起動SceneのアクションをVStackへまとめると意図した表示にならず、NewDocumentButtonを直接配置した。Boolと別の形式値で出力シートを表示すると初回に古い形式が出る問題があり、itemで内容を決めるよう修正した。文字拡大は縦並びにするだけではキャンバスを圧迫するため、最大文字サイズで確認した。

記事ネタ候補

文書アプリで制作ガイドと標準ファイル管理を両立する設計。パネル再分類で入力状態を保つ構成。UI規約と実操作の確認記録を分ける運用。

刺しゅうの統計と仮定を変更できる推定時間

feature開発中(未リリース)

刺しゅうの統計と仮定を変更できる推定時間

何を変えたか

開発中・未リリース。生成針から針数・色数・色ブロック数・色替え・ジャンプ・糸切り・停止回数、縫製寸法、移動距離を集計する表示を追加した。針/分・移動速度・命令待ち時間を変更して推定時間を確認できる。

なぜ変えたか

基本制作フローの仕上げとして、書き出す図案の規模と工程を判断できる情報を揃えるため。

ユーザーにとって何が良くなったか

同色の再使用と実際の色数を区別し、ジャンプだけの点を含まない縫製寸法を確認できる。機種ごとの仮定を変えて時間の概算を比較できる。

技術的な内容

Swiftの命令列を一度走査する。針を含むブロックのRGBのみ集合へ追加し、糸情報が欠ければ色数を不明とする。針座標の外接範囲と、枠中心からの縫い/ジャンプ距離を別計算する。UIは結果変更時に非同期集計し、生成中や失敗時には旧統計を表示しない。正本と保存形式は変更しない。

ハマったこと・学んだこと

色替え回数+1と実際の色数は一致しない。空色ブロックや同色再使用を区別する必要がある。距離計算の始点と縫製寸法の対象点も異なる。

判断したこと・トレードオフ

推定時間は仮定に基づく計算値で、実機計測値として扱わない。設定は画面内だけに保持する。機種ごとの加減速や糸交換作業は未検証。

選択部分だけの針データ変換

feature開発中(未リリース)

選択部分だけの針データ変換

開発中・未リリース。選択した刺しゅうオブジェクトだけを針データへ変換し、1針編集へ移れるようにした。

変更と目的

従来は表示中の図案全体を一つの針データへ変換していた。部分的に針を直したいとき、他の文字や面の再編集性まで失うため、選択したオブジェクトだけをそれぞれの縫い順位置で変換するようにした。グループは全体を対象にし、変換時に解除する。非選択の正本は保持し、単一Undoで元へ戻せる。

技術的な知見

取込データの経路を流用すると、同じ糸でもオブジェクト境界に色替えが追加され、毎回機械原点を経由してしまう。schema21で生成由来の針正本に継続情報を持たせ、糸が一致するときだけ継続し、入口は前の図形の終点から決めるようにした。旧取込データの強制境界は維持する。針編集で糸が未指定になれば色替え境界を維持する。

また、生成由来の針では座標計算用の原点へ移動しないため、原点が枠外でも実際の針が枠内なら許容する。レビューで発見した移動後の失敗を再現試験で確認し、未使用原点を表示/品質検証用の経路にも含めないよう修正した。

検証・残り

選択外保持、同糸境界、異色境界、共通端点、グループ、保存形式移行、Undo、原点が枠外になる移動と針再編集を自動試験で確認する。全体機能の手動・実機縫製受入は未実施。アプリ内お知らせ機能はなく、未リリースの開発記録として残す。

最終検証: 全401件成功、失敗・スキップ・実行時警告0。strict整形検査・独立レビュー済み。原点を表示経路から除いた後も、変形用には原点を明示的に含めた座標列を作り、コマンドとの対応を維持する。

品質問題の位置へ移動する

feature開発中(未リリース)

品質問題の位置へ移動する

何を変えたか

開発中・未リリース。品質確認を修正必須・注意・情報に分け、指摘を選ぶと該当位置を中央へ移動して強調する。生成針データでは命令番号と針番号も表示する。

なぜ変えたか

問題の説明だけでは大きな図案の修正場所を探しにくいため。

ユーザーにとって何が良くなったか

各種類の最初の問題位置へ移動できる。未確認の機種条件は情報として表示し、互換性を推測しない。位置不明の場合は架空の場所を表示しない。

技術的な内容

生成命令を一度走査し、stitchだけ針番号を進める。jumpには針番号を付けず、stopには架空のオブジェクトIDを付けない。選択位置は文書revision、入力世代、生成要求ID、指摘tokenで検証し、古い操作を拒否する。中心表示は既存mapperで画面座標との差を計算し、図案を変えずpanだけを更新する。

ハマったこと・学んだこと

問題の対象を選ぶ操作自体が入力世代を変える。世代変更後の指摘tokenへ対応付け直してから強調表示を作る必要があった。入力途中の手動点列を破棄しないよう、位置移動の入口にも状態検査を置く。

生地と汎用機種の制作設定

feature開発中(未リリース)

生地と汎用機種の制作設定

何を変えたか

開発中・未リリース。生地区分と補助材メモ、機種の表示名、利用者指定の最大針目長・最大ジャンプ長、糸切り・停止対応を作品へ保存する設定を追加した。違反する生成データの書き出しを禁止する。

なぜ変えたか

基本仕様の制作条件を保存し、使用者が確認した機械の制限を品質確認に反映するため。

ユーザーにとって何が良くなったか

作品ごとの条件を保存・再編集できる。未確認の能力は未確認のまま扱い、生地変更で密度や輪郭を勝手に変更しない。設定適用とディスク保存は表示上も区別する。

技術的な内容

optional設定をschema 22へ追加し、旧文書の未指定を保持する。設定変更は単一Undoに対応。枠中心を初期位置として、元座標と0.1mm丸め後の双方で区間長を検査する。DSTの糸切りに含まれる約0.566mmの移動は、その形式の出力時に別途検査する。

ハマったこと・学んだこと

論理上の糸切りでも、形式によって実際の移動命令に展開される。共通の針データ検査と形式固有の検査を分ける必要があった。レビューでは設定変更が入力途中の手動点列を取り消す問題も見つかり、入力中の適用・解除を防いだ。

許容誤差を指定して輪郭の点を減らす

feature開発中(未リリース)

許容誤差を指定して輪郭の点を減らす

開発中・未リリース。画像トレースや手描きの輪郭に増えた点を、0.01〜5mmの誤差を指定して整理する機能を追加した。

変更と目的

通常のランニング/ビーン線とタタミの外周・穴を対象に、選択輪郭だけの点数を減らす。線の両端と閉輪郭の始点を残し、後からノードを編集できる状態を維持する。Undoと保存再編集に対応する。文字・曲線・サテン・手動針は正本の意味が異なるため対象外。

技術的な知見

反復型Ramer–Douglas–Peucker方式を独自に実装した。再帰を避け、入力10万点と比較200万回を上限にする。閉輪郭は先頭点と最遠点で2区間に分け、3点未満になる結果なら元を維持する。

距離誤差を守るだけでは面の妥当性は守れない。外周の突出部を削ると、内部の小さい穴が外側へ出る場合がある。既存の面検証を保存前に通し、不正なら文書を変更せず拒否する。処理をバックグラウンドで行い、文書・選択・対象輪郭が変われば古い結果を適用しない。

検証と残り

線分からの距離誤差、端点・閉輪郭始点、小さい輪郭、穴の維持、選択穴だけの編集、保存・Undo、計算量、取消と古い結果の拒否を試験する。点数削減と曲線への平滑化は区別し、平滑化は残タスク。手動・ミシン受入は最終工程。アプリ内のお知らせ機能はなく、未リリース開発記録として残す。

検証結果: 全体412件中411件成功。残る1件はUndoで外周選択へ戻ることを考慮していないテスト準備の問題で、穴を再選択してから実行するよう修正し、関連統合2件の再試験が成功した。全体試験後の製品コード変更なし。実行時警告0、独立レビュー・strict整形検査済み。

数値で移動・回転・拡縮する

feature開発中(未リリース)

数値で移動・回転・拡縮する

何を変えたか

開発中・未リリース。移動量mm、回転量、現在の大きさに対する倍率%を指定して、選択した図案へ明示的に適用する操作を追加した。固定幅の操作も保持する。

なぜ変えたか

1mm・90度・固定倍率だけでは細かな配置調整が難しいため。

ユーザーにとって何が良くなったか

小数や負の移動量、任意の回転量、倍率を指定できる。複数選択は共通の中心でまとめて変形し、一回のUndoで戻せる。

技術的な内容

入力文字列を有限値へ検証してから既存の変形処理へ渡す。選択全体の外接矩形中心は操作時に計算し、描画時点の古い中心を使わない。ロックと未確定入力を守り、生成用針データへ平坦化せず編集正本を変形する。

ハマったこと・学んだこと

数値入力を追加するだけでは、固定ボタンと新操作で回転中心がずれる可能性がある。中心計算と適用入口を共通化し、現在の選択を操作時に評価する構造にした。

Magic Wandで画像の一部を刺しゅう面にする

feature開発中(未リリース)

Magic Wandで画像の一部を刺しゅう面にする

開発中・未リリース。下絵の色をタップし、指定した色差の領域を編集可能なタタミとして追加する機能を実装した。

変更と目的

画像全体の自動変換だけでなく、必要な領域だけを手動で指定できるようにする。色の許容差、連結領域だけ/画像全体、解析解像度、タタミ間隔と角度を指定できる。追加後は外周・穴・糸色などを編集し、保存やUndoを利用できる。

技術的な知見

基準画素とのRGBユークリッド距離を最大距離255√3で正規化して比較する。隣接画素の色を次々に基準へ変える方式ではグラデーションに沿って選択が広がりすぎるため、基準は最初の画素に固定する。標準は上下左右の4近傍で探索し、非連結モードでは全画素を比較する。透明度128未満は選択しない。

選択マスクだけを既存の輪郭抽出器へ渡すことで、穴と分離した領域の正本、複雑度制限、生成検証を共通化した。画像表示上の相対位置を解析後の画素位置に変換し、元の下絵のmm配置へ戻す。糸色は基準色に統一し、追加後に手動調整できる。

検証と制約

固定基準色、4近傍、分離領域、穴、透明、不正入力、画像上下の座標対応、保存・Undoを自動テストで確認する。解析長辺は最大256画素で、縮小により細部が変わる。輪郭の角を少し落とす既存方式を使用する。物体の意味認識・写真刺しゅうとは別機能。手動・ミシン受入は最終工程。アプリ内お知らせ機能はなく、未リリース開発記録として扱う。

最終検証: 全420件成功、失敗・スキップ・実行時警告0。独立レビューで見つかった生成中の再タップによるマーカーと結果の食い違いは、生成中の画像hit testingを無効にして修正した。再レビューとstrict整形検査も完了。

手描きラインの角を丸めて編集可能な曲線へ

feature開発中(未リリース)

手描きラインの角を丸めて編集可能な曲線へ

何を変えたか

開発中・未リリース。通常のランニング・豆縫いの点列を、許容誤差を指定して複数区間のベジェ曲線へ変換する明示操作を追加した。

なぜ変えたか

手描きの角を滑らかにし、変換後も制御点を編集できるようにするため。

ユーザーにとって何が良くなったか

開線の端点、閉線の縫い始め、糸・順序・針目長を保持したまま曲線化できる。保存・再読込と一回のUndoに対応する。面・穴・サテンの曲線化は今回の対象に含めない。

技術的な内容

頂点の前後を許容誤差と辺長に応じて切り取り、頂点を両制御点とする三次曲線で接続する。曲線は切取点と元頂点の三角形内に収まる。同じラインの別の辺や角と接触し得る箇所では元の角を維持する。既存のBezierPathを正本とし、生成用点列を別途計算する。

ハマったこと・学んだこと

平滑化で見た目だけを整えると近接する線に交差を追加し得るため、局所的な変位制限と周囲との接触検査を組み合わせる設計にした。点数削減は別操作として残し、自動で先行適用しない。

刺しゅう制作の操作モードと5段階ガイド

feature開発中(未リリース)

刺しゅう制作の操作モードと5段階ガイド

何を変えたか

開発中・未リリース。かんたん/デジタイズ/プロで表示する編集操作の深さを切り替える機能と、素材→トレース/変換→編集→プレビュー/品質→保存/出力のガイドを追加した。

なぜ変えたか

基本仕様の監査で、制作機能は増えていても初心者向けの操作案内とモード分離が不足していたため。

ユーザーにとって何が良くなったか

全モードで基本素材入力・作成・糸色・縫い順・出力を使える。詳細編集が必要な図案には切替導線を表示する。ガイドは任意段階へ移動し、閉じて再開できる。

技術的な内容

SwiftUIの表示状態とプロジェクト正本を分離した。モードを切り替えても選択とUndo/Redo履歴を保持する。未確定の手動点・穴入力を取り消し、入力世代を変えて切替前の遅いPencil通知を拒否する。同じモードの再設定は入力を破棄しない。

ハマったこと・学んだこと

詳細操作を隠すだけでは、選択中の図案に必要な編集方法を見失う。段階的な表示と、必要なモードへ移る説明を一緒に実装した。ガイド上の次操作は実際のボタン名と一致させ、保存や実機確認の完了は自動判定しない。

読めない下書きを保全して保存を続ける

bugfix開発中(未リリース)

読めない下書きを保全して保存を続ける

何を変えたか

開発中・未リリース。同じ文書IDの復旧用下書きが破損した場合、元データを別名で保全してから新しい正常な下書きを保存する。

なぜ変えたか

旧実装は保存前の読込が失敗すると、新しい編集内容も継続的に保護できなかったため。

ユーザーにとって何が良くなったか

読めない下書きを失わず保存を再開できる。保全データは自動復元候補へ混ぜず、読み込めない下書きの一覧から明示的に破棄できる。

技術的な内容

読み取り・権限の失敗と、JSONやプロジェクトの検証失敗を区別する。保全に失敗したら上書きせず、新しい保存が失敗しても元データを残す。キャンセルを破損と解釈しない。一覧は正常完了時のみ更新し、保存成功後は古い破損行から正常な新データを削除できないようにする。

ハマったこと・学んだこと

原子的な書き込みだけでは、保存前の古いデータの検査が保存を阻む問題は解決しない。また時刻比較で保存をスキップした場合は、その結果をUIへ返さないと保護済みと誤表示してしまう。

指とPencilの入力設定と二本指操作の仲裁

feature開発中(未リリース)

指とPencilの入力設定と二本指操作の仲裁

何を変えたか

開発中・未リリース。描画と手動針落ちの入力を、Pencil専用または指/Pencil両対応へ切り替えられるようにした。二本指pan/pinchは維持し、ナビゲーション開始時には未確定の指描画を破棄する。

なぜ変えたか

基本仕様にある入力方法の設定を実装し、Pencilなしでも指で制作できるようにするため。

ユーザーにとって何が良くなったか

入力方法を選べる。変更前に始まった入力が遅れて届いても、図案へ混入しない。確定した図案・選択・Undo/Redoは維持する。

技術的な内容

PKCanvasViewのdrawingPolicyと手動点recognizerのallowedTouchTypesをmake/update両方へ反映。入力開始時の座標変換と世代を固定する。pan/pinchを集合で管理し、両方が終わるまで描画を再開しない。

ハマったこと・学んだこと

入力ポリシーによる早期returnがgesture終了処理を飛ばすと、操作中の設定変更で描画無効状態が残る。レビューで発見し、terminal cleanupをポリシー分岐より先へ移した。実タッチのないUIKit試験ではtranslationを明示したrecognizerが必要だった。

サテン幅とフィル行間隔の注意を追加

feature開発中(未リリース)

サテン幅とフィル行間隔の注意を追加

何を変えたか

開発中・未リリース。狭いサテンと詰まったフィル行間隔に対して、編集正本に基づく品質の注意を追加した。

なぜ変えたか

短い針目の警告だけでは、幅や行間隔を見直すべき理由が伝わらないため。基本仕様の最終照合で不足を発見した。

ユーザーにとって何が良くなったか

注意を選択して対象位置を確認し、サテンのレールやフィルの主縫い・下縫いの設定を調整できる。目安による注意で自動的に書き出しを止めない。

技術的な内容

サテンは対応する左右レールの線形補間区間における最小幅を調べる。頂点同士の幅だけを見ると、区間の途中にある狭い部分を見逃すため。フィルは主縫いと下縫いの各設定を個別に検査する。

ハマったこと・学んだこと

設定から分かる問題と、生成された針から分かる問題では位置の意味が異なる。設定対象の輪郭位置を示す場合はその意味を明記し、存在しない針番号を付けない。

基本機能の最終自動確認

基本機能の実装台帳は、操作モード・案内、入力方法、制作設定、品質の場所と分類、復旧保全、集計、ライン平滑化、数値変形、文字拡大と代替操作まで実装・自動検証を終えた。実ファイル保存再読込から4形式出力までの業務層統合も成功した。UIを操作するE2Eではない。

最終Simulator測定は1万入力点から99,139針を0.049198459秒で生成し、取消要求後0.000749875秒で終了した。これは最小対応iPadの測定ではない。署名配布、実機起動、Pencil・Files・VoiceOver・長時間編集・ミシン試縫いの受入は未実施であり、総合100%やリリース済みとは扱わない。

編集可能な面図形の結合・差分

feature開発中(未リリース)

編集可能な面図形の結合・差分

開発中・未リリース。通常のタタミ面2つに対する結合・交差・差分・排他的差分を追加した。

何を変えたか・なぜ変えたか

画像化や針データへの変換をせず、輪郭を組み合わせて刺しゅう形状を作れるようにした。穴と分離領域を編集可能な正本として保持する。基準面の糸色・縫い設定・レイヤー・縫い順位置を継承し、Undo一回で元の2面へ戻せる。結果が空になる演算は両面を削除することを画面で説明する。

技術的な内容・学んだこと

iPad標準のCore Graphicsで面演算し、結果の包含関係から外周と穴を分類する。結果が点接触の穴を持つ場合、既存の厳密な単純多角形検証では保存できなかった。回帰試験で再現し、その場合だけ頂点のY座標で水平帯に分け、境界の上下端を補間して三角形・台形へ分解した。人工的な隙間を開けず、面積を保って再編集できる。

分解後は複数の刺しゅうオブジェクトとなり、それぞれに下縫い・留め縫いが適用される。プレビューと最終縫製確認が必要。計算量と文書上限を検証してから単一トランザクションで適用し、処理中に選択や文書が変われば結果を破棄する。

検証と残り

全395件の自動テスト成功、失敗・スキップ・実行時警告0。面積、点/辺の接触、穴同士の接触、微小隙間、座標平行移動、計算量上限、保存・Undo、取消・古い結果の拒否を確認した。独立レビュー済み。手動・Pencil・ミシン受入は最終工程。文字や針データに対する面演算、ナイフ分割は今回の対象外。アプリ内お知らせ機能は確認できず、未リリースの開発記録として残す。

文字拡大と基本制作フローの仕上げ

feature開発中(未リリース)

文字拡大と基本制作フローの仕上げ

何を変えたか

開発中・未リリース。狭い画面や大きな文字でも編集欄を利用できるようにし、キャンバス表示の移動・拡縮、手動針落ち、針順確認にジェスチャー以外の操作を用意した。

なぜ変えたか

固定幅の編集欄とドラッグ中心の操作では、ウィンドウ幅や文字サイズ、入力方法によって基本操作が難しくなるため。

ユーザーにとって何が良くなったか

編集欄の配置が変わっても未適用の入力を維持し、必要に応じて閉じてキャンバスを広く使える。手動針落ちは座標からも追加でき、針順は前後のボタンでも確認できる。

技術的な内容

編集欄を別の画面へ移し替えず、同じビューを維持して幅と表示位置を切り替える。点のドラッグでは開始時の座標変換と対象を保持し、途中の選択・表示サイズ変更による古い操作を拒否する。

ハマったこと・学んだこと

画面サイズ変更のために文書入力全体の世代を変えると、未確定の手動針や品質位置まで無効になる。表示領域の世代を分けることで、ドラッグの失効と編集途中のデータ保持を両立する。

文書内の糸ライブラリと図案への割り当て

feature開発中(未リリース)

文書内の糸ライブラリと図案への割り当て

開発中・未リリース。名称・メーカー・カタログ番号・チャート・RGB/HEX色を登録し、検索・近似色順表示から通常図案とクロスパレットへ割り当てる機能を追加した。ライブラリは最大1,000件を文書内へ保存する。市販カタログは同梱していない。

RGBだけでは糸の同一性を表せない

同じ画面色でも、メーカーや糸番号が異なる糸を使う場合がある。schema17で糸情報を正本に保持し、生成時もRGBだけで糸替えを統合しない。PESにはメーカー等を引き継ぎ、JEFの形式パレット番号とは区別する。DST/EXPには糸メタデータがない。

割り当てはコピーとして扱う。登録内容の変更・削除で既存図案を一斉変更せず、ユーザーが再割り当てする。色を直接変更した場合は元のメーカー・番号を残して誤った情報にしないよう割り当て情報を解除する。Undo/Redoと保存再編集を維持する。

検索は名称・メーカー・番号・チャートの部分一致。類似色はsRGB距離で順位付けし、同距離は登録順にする。これは画面色の近似で、実物の糸色同等性を保証しない。

検証で見つかった既存操作との関係

線の結合がRGBだけで一致判定していたため、異なる割り当て糸を持つ線を結合すると一方の情報を失うことを発見した。失敗する回帰テストを追加してから、割り当て糸の一致も必須にした。通常/Crossの割り当て、色変更時の解除、検索、保存、旧形式移行、Undo、PES往復をテストした。独立レビューでは既知の線結合問題以外の指摘なし。

取込針の糸表編集、文書間ライブラリ共有、市販カタログの収録は残項目。手動・実機受入は最終工程。この更新はリリース告知ではない。

最終全回帰350件成功、失敗・スキップ・実行時警告0。書式検査も成功。

編集可能な図案の糸色をHEXで入力する

feature開発中(未リリース)

編集可能な図案の糸色をHEXで入力する

開発中・未リリース。通常図案の糸色とクロスステッチのパレット色へHEX入力を追加した。色を数値で再現・共有しやすくするため、既存のカラーピッカーと併用する。

3桁と6桁、先頭の#省略、大文字小文字と前後空白を許容する。ASCIIの16進数字だけを解析し、透明度8桁・余分な文字・全角数字は拒否する。表示は大文字#RRGGBBへ統一し、浮動小数のRGBは8bitへ最近接丸めする。

表示の丸めで保存色が変わらないよう、適用ボタンを押したときだけ既存の編集コマンドを呼ぶ。選択の切替や外部の色変更時は表示を同期する。Undo/Redoと保存は通常の文書編集経路を使い、取込針の糸表は対象外とする。

変換の成功・異常値・丸めをCoreで検証し、通常図案とクロスの色を変更して保存往復・Undo/Redoする統合試験を追加した。メーカーの糸番号・実際の糸色照合や類似色検索は別の残項目。手動受入は最終工程に残す。

最終の全343件が成功し、失敗・スキップ・実行時警告は0。独立レビューで不具合の指摘なし。

SVG図形とグループの変換を取り込む

feature開発中(未リリース)

SVG図形とグループの変換を取り込む

開発中・未リリース。手動操作・ミシン縫製は最終工程に残す。

何を変えたか

図形とグループに指定されたtranslate、scale、rotate、skewX/skewY、matrixを取り込めるようにした。回転中心、反転、入れ子のグループにも対応する。変換後の編集可能な輪郭として保存する。

なぜ変えたか / 利用者への効果

外部のベクトル編集ソフトで位置やサイズを調整したSVGを取り込むため。従来はtransform属性を含む文書全体を拒否していた。

技術的な内容

アフィン行列を親×子の順で合成し、viewBoxのmm換算より前に適用する。曲線は変換済み制御点を使って分割する。兄弟要素に直前の変換が残らないよう、XML要素と同じ深さの行列スタックを使う。

判断と学び

変換リストの順序とグループの入れ子を別々の規則で実装すると結果が食い違う。どちらも同じ行列合成へそろえ、数値例で同一になることを検証した。ベジェ曲線は変換前に近似すると拡大で誤差が増えるため、変換とmm換算の後に分割する。

検証と残項目

合同224件成功(Core131 + App93)、失敗・スキップ・実行時警告なし。合成順、回転中心、反転、せん断、全基本図形、兄弟への漏れ防止、不正値・退化・上限・取消、保存・Undo・DST/EXPを確認した。独立レビューで具体的な不具合指摘なし。

rootのtransform、CSS変形、円弧Aは未対応。円/楕円は従来の64点近似を変換する。SVGの変換設定や制御点そのものは保存せず、取り込み後は点列として編集する。

角丸矩形の取り込みと拡大円・楕円の輪郭精度

feature開発中(未リリース)

角丸矩形の取り込みと拡大円・楕円の輪郭精度

開発中・未リリース。手動・実機・ミシン検証は最終工程で行う。

変更と効果

SVGのrectにrx/ryを指定した角丸矩形を取り込めるようにした。片方省略時は他方の半径を使い、幅・高さの半分へ制限する。ゼロ半径は通常矩形、負値・非有限値は拒否する。

円・楕円も固定64点近似から、変換後のmm座標で許容誤差0.05mmに収まる適応分割へ変更した。拡大時の輪郭の粗さを抑える。元の半径設定を保存する機能ではなく、取り込んだ点列が編集正本となる。

設計と知見

基本図形を内部のM/L/A/Z経路に変換して既存のSVGPathReaderを使った。変換順、曲線分割、取消、点数上限を共通化し、図形だけ再変換されることを避けた。半径200mmの円で従来約0.24mmだった弦の中点誤差を先にテストで検出し、0.05mm以下への改善を確認した。

等価な楕円でも、変換を後から適用する場合と寸法へ織り込む場合では数値の丸めがわずかに異なる。幾何の等価性テストには十分小さい1e-10の許容差を使い、保存往復は完全一致で検証した。

検証と残項目

242件(Core145 + App97)成功。失敗・スキップ・実行時警告なし。独立レビュー指摘なし。半径規則、変換後精度、等価パス、過大入力拒否、保存、Undo/Redo、DST/EXPを検証した。

半径の%や長さ単位、CSSは未対応。アプリ内の図形作成は今回の変更対象外。縫製品質の確認は未実施。アプリにお知らせ機能はなく、未リリースの知見として記録する。

SVG全体の変換を取り込みへ反映

feature開発中(未リリース)

SVG全体の変換を取り込みへ反映

開発中・未リリース。手動・実機・ミシン確認は最終工程で行う。

変更と効果

rootのtransformに対応した。SVG全体の移動・拡縮・回転・せん断・行列変換を含むファイルを編集輪郭へ取り込める。保存スキーマは10のまま。

設計と学び

図形/g変換、viewBoxによるmm換算、root変換の順に適用する。rootの移動成分はpxからmmへ換算し、SVG2の既定transform-originであるviewport中央を前後移動として合成する。root行列を子のスタックに混ぜるとviewBoxとの順序を誤るため別管理した。

初回レビューで既定中心の見落としを検出した。仕様を確認して期待値を修正し、失敗を再現してから実装修正した。単に全テストが通るだけでは、テストと実装が共有した仕様の誤読は検出できない。

検証と制約

238件(Core142 + App96)成功、失敗・スキップ・実行時警告なし。変換順、単位、回転中心、曲線分割、保存、Undo/Redo、DST/EXP、不正入力の原子的拒否を確認。再レビューで追加指摘なし。

transform-origin/transform-boxの明示指定、CSSは未対応。縫製品質は未確認。アプリにお知らせ機能はなく、未リリースの開発記録として残す。

SVGの円弧・楕円弧を編集輪郭として取り込む

feature開発中(未リリース)

SVGの円弧・楕円弧を編集輪郭として取り込む

開発中・未リリース。手動操作・実機・ミシン検証は最終工程で行う。

何を変えたか

SVGのA/aコマンドを追加した。回転楕円、弧の向き、大きい側の弧の選択、相対・反復指定、連続した0/1フラグを読み込む。半径不足を補正し、ゼロ半径は直線、同一端点は追加なしとして扱う。

なぜ変えたか / 利用者への効果

ベジェ曲線に続き、円弧を含む輪郭も外部ソフトから取り込めるようにするため。変換後の点列を編集・保存し、生成・出力へ進められる。

技術的な内容

仕様の端点形式を中心形式に変換し、楕円の解析式から直接点列を生成する。変換とmm換算後の楕円軸ベクトルから曲率に関わる上界を求め、線形補間誤差が0.05mm以内になるよう分割数を決める。端点は元入力を正確に維持する。

判断と学び

円弧をまずベジェへ変換すると近似誤差が二段階になる。今回は楕円から直接分割することで、最終mm座標で誤差を管理した。また、SVGのフラグは一般の数値と異なり1文字ずつ読むため、引数位置に応じた字句解析が必要だった。

検証と残項目

合同231件成功(Core137 + App94)、失敗・スキップ・実行時警告なし。4通りの弧、回転楕円、変換後の誤差、半径補正、壊れた入力、点数上限・取消、保存・Undo・DST/EXP出力を確認した。独立レビューに具体的な不具合指摘なし。

取り込み後は点列で編集し、円弧の元パラメータは保持しない。root変換・CSS・文字等は未対応。縫製品質の確認は未実施。

SVGのベジェ曲線を刺しゅう用の編集輪郭へ取り込む

feature開発中(未リリース)

SVGのベジェ曲線を刺しゅう用の編集輪郭へ取り込む

開発中・未リリース。手動操作・実機・試縫いは最終工程で確認する。

何を変えたか

SVGのC/S/Q/Tとその相対指定を読み込み、黒のランニングとして追加する機能を実装した。連続区間や省略された制御点にも対応する。保存するのは編集可能な点列で、SVGの制御点は保持しない。保存・Undo・DST/EXP出力に接続した。

なぜ変えたか / 利用者への効果

従来は曲線のあるSVGを拒否していた。外部で作ったベジェ輪郭を取り込み、刺しゅうの輪郭編集へ進めるようにした。

技術的な内容

viewBoxと寸法からmmへ変換した制御点を使い、0.05mmを許容誤差として再帰分割する。2次曲線は3次へ変換して既存の分割処理を使用する。相対座標の現在位置はSVG座標で保持し、出力点だけをmmへ変換する。

判断と学び

SVG座標で先に近似してから大きく拡大すると、刺しゅうの実寸で誤差も増える。分割をmm換算後に行うことで、縦横の拡大率が違っても同じ精度基準を使える。SとTの反転制御点は直前の対応する曲線だけから引き継ぎ、移動・直線・閉路で履歴を解除する必要があった。

自動検証と残項目

合同217件成功(Core125 + App92)、失敗・スキップ・実行時警告なし。相対座標、反復、反転履歴、閉路後の再開、異方的拡大、解析式からの距離、過大入力・キャンセル、保存と出力を検証した。独立コードレビューに具体的バグ指摘なし。

円弧A、transform、CSS、文字などは未対応で、含まれるファイル全体を拒否する。色・塗り・線幅は取り込まない。複数区間の制御点を保った編集も残作業。

SVGのviewBox配置と拡大方法を取り込みへ反映

feature開発中(未リリース)

SVGのviewBox配置と拡大方法を取り込みへ反映

開発中・未リリース。手動・実機・ミシン検証は最終工程で行う。

変更と利用者への効果

preserveAspectRatioの9配置、meet/slice/noneに対応した。これまで中央配置とnoneに限定していたSVG入力で、左右・上下の配置指定を含む輪郭も取り込める。編集用の輪郭として保存し、生成・書き出しへ進める。

実装と知見

meetは縦横の小さい倍率、sliceは大きい倍率を採用する。余白にMin=0、Mid=0.5、Max=1を掛けて配置し、sliceでは負の余白もそのまま適用する。noneは縦横別倍率で、後続のmeet/slice指定を無視する。viewBoxがなければ配置指定は座標に影響しない。

既存のmm変換経路を利用し、変形済み曲線も最終座標で近似する。viewport外の輪郭は従来どおり切り取らず保持し、この仕様をUIにも示した。クリップ対応とは区別している。保存スキーマは10のまま。

検証と残項目

234件成功(Core139 + App95)、失敗・スキップ・実行時警告なし。36組の配置・倍率、既定値・不正入力、保存、Undo/Redo、DST/EXP出力を確認。独立レビューで具体的な不具合指摘なし。

root transformやCSS、clipPathは未対応。縫製品質はまだ確認していない。アプリにお知らせ機能はなく、開発記録として残す。

PNG/JPEGから編集可能なタタミを生成する

feature開発中(未リリース)

PNG/JPEGから編集可能なタタミを生成する

開発中・未リリース。下絵として取り込んだPNG/JPEGから、減色・背景除去・小領域除去・輪郭と穴の抽出を行い、通常のタタミオブジェクトを作成できるようになった。画像から図案生成、輪郭/穴/糸色/密度/角度の編集、保存・再編集、PES/DST/JEF/EXP書き出しまでを接続した。

Brotherの画像変換の公式説明では、色数指定の一方向タタミとCross Stitchを別方式としている。今回の実装はタタミ方式。機能と制作フローを参考に、減色・領域抽出・境界追跡は独自実装した。

画像正本と編集できる面

長辺64/128/256pxの解析画像をsRGB RGBAへ変換し、非乗算色で減色する。透明度50%未満を除外、5bitRGBヒストグラムと重み付きk-meansで最大16色へまとめる。背景除去は画像端の最頻色に属し、端から上下左右につながる画素のみを対象とする。内側にある同色や穴は残す。小領域は実寸面積で除き、除外画素数を表示する。

向き付き境界を右折優先で追跡し、直線上の不要点を除去する。角を1/8解析画素だけ落として対角接触を分離し、外周と穴を編集用の閉輪郭へ変換する。色ごと・同色内の領域走査順に追加する。これは平滑なベジェ復元や写真刺しゅうではなく、明確なイラスト向けの画素輪郭近似である。

元画像と既存図案は保持し、追加全体はUndo一回で戻る。解析はバックグラウンドで実行し、取消・編集状態の変更・画面離脱後に古い結果を適用しない。ロックした追加先や枠外画像、領域/点数/針数/処理量上限は明示的に拒否し、一部だけを黙って追加しない。再実行は追加になるため、作り直す場合は前の生成分をUndo/削除する。

検証と残り

Core9件/App3件を追加し、全321件(Core209/App112)の自動試験が成功した。失敗・スキップ・実行時警告なし。3×3画素の全511非空パターンについて、元画素中心と生成した穴付き面の内外を照合。二色・背景/内側同色・透明度・ノイズ・上限・取消・Undo・保存・編集を確認した。Appでは非対称RGBAの上下/色と、PNG/JPEGから密度編集・4形式出力までを検証し、独立レビューで重大な指摘なし。

高精細図案の平滑化、自動の縫い方式/方向選択、Photo/Cross/Paint/Sketchは別の残項目。Janomeのv2.0公式資料で発見したArtistic Digitizerのノード平滑化、サテン端ランダム、取込針密度調整・自動留め縫いも機能表へ追加した。見た目・Pencil・実作品の性能・実機縫製は最終受入に残る。アプリ内お知らせ機能はなく、未リリースの開発記録として掲載する。

PES/PEC取り込みと糸情報・糸切りの保持

feature開発中(未リリース)

PES/PEC取り込みと糸情報・糸切りの保持

開発中・未リリース。FilesからPES/PECを読み込み、機械用の針データを編集可能な正本へ取り込む処理を追加した。針位置、ジャンプ、糸切り、停止、色替えを保持し、保存形式15で再編集・変形・Undo/Redoできる。PES内の高次の輪郭や文字オブジェクトを復元するものではない。

RGB、糸名称、糸番号、メーカー、チャートが格納されている場合は共通の糸表へ保存する。色替えの挿入/削除で糸表も更新し、位置編集や変形で情報を失わない。JEFとPESのプレビューは色ブロック別に描画し、命令選択時に現在の糸情報を表示する。生成を無効にしていても取込針の正本を描く。

解析と検証

公開されたpyembroideryのreader/writerを形式照合に用い、解析処理は独自実装した。標準PECパレットのRGB数値のみMITの利用条件に従って使用し、出典とライセンスを同梱している。

入力を4MiB、PEC色枠を255、命令を30万、針を10万に制限する。全読み取りをファイル・ヘッダ・針ブロックの境界内で行い、長さや色表の不整合、未知フラグ、切断を拒否する。未対応の特殊fill/motif/feather定義が糸表より前にある場合も、色を黙って標準色へ落とさず拒否する。

独立writerで生成したPES v1/v6の自作5針fixtureを使い、非対称座標・糸切り3回・停止1回・色替え・架空の糸番号とメーカーを照合した。全295件(Core189/App106)の自動試験が成功し、失敗・スキップ・実行時警告なし。

レビューでは先頭trimの取り込み後、枠中央にあると書き出せない不具合を発見した。機械の現在位置をゼロ移動ジャンプで明示することで、針落ちを増やさずDST/EXP/JEFへ出力できるよう修正した。回帰テストと再レビュー済み。

残る対応

PESのv1〜v10にある既知のヘッダ位置を解析するが、独立fixtureで照合した版はv1/v6。他の版・実ファイルの互換性拡充、PES書き出し、特殊パターン定義への対応が残る。同じ糸情報が連続する色境界を停止と読む慣習や、糸切りの機種別解釈も実機での確認が必要。

手動の画面・Pencil・Files確認と実機縫製は指定どおり最終工程。製品全体の完成ではない。アプリにお知らせ機能はなく、未リリースの開発記録として公開する。

糸情報と縮小図を含むPES v6書き出し

feature開発中(未リリース)

糸情報と縮小図を含むPES v6書き出し

開発中・未リリース。書き出し形式にPESを追加し、PES/DST/JEFの入出力経路がそろった。PES v6の糸表へRGB・名称・糸番号・メーカー・チャートを保存し、機械用PEC命令と48×38pxの全体/色区間別縮小図を生成する。高次の編集オブジェクトを含まないcompact v6で、輪郭・文字の編集正本はプロジェクト形式に残す。

針位置は枠中央基準の0.1mmへ丸め、長いジャンプだけを分割する。針落ちは追加しない。糸切りはゼロ移動のtrimフラグ、停止は同じ糸情報を繰り返す慣習で記録する。縮小図には縫い線だけを描き、ジャンプを描かない。

情報を黙って変更しない

同一糸への通常色替えはPESの停止と区別できないため明示的エラーにする。RGBを8bit化した結果の一致や空の文字列も判定に含める。別の糸番号・名称を捏造して回避せず、その場合はJEFを利用する。

名称と糸情報はUTF-8で各255byteまでとし、超過時に文字列を切り捨てない。停止を含む色枠255、針10万、出力命令30万、ファイル4MiBまで。PES糸表を解釈しない機器/ソフト向けにはPECの近似色を記録するため、表示色が異なる場合がある。

検証と残り

全300件(Core194/App106)の自動試験が成功し、失敗・スキップ・実行時警告なし。独立したpyembroidery 1.5.1 readerで自作fixtureの5針、糸切り・停止・色替え各1回、日本語の糸名称と糸番号・メーカー・RGB、4枚の縮小図を照合した。長いジャンプ、色枠上限、文字列/座標の異常値、取消、画像にジャンプを描かないことも検証した。独立レビューで重大な指摘なし。

形式の公開実装を参照し、命令生成・画像描画は独自実装。手動/Pencil/Files・サムネイル表示・機種別停止/糸切り・実機縫製の確認は最後に残している。すべてのPES版やメーカー機種への互換性を保証する段階ではなく、製品全体も未完成。次は生成した針を直接編集する機能へ進む。アプリ内お知らせ機能はなく、未リリースの開発記事として記録する。

針・ジャンプ・色替えの追加と削除

feature開発中(未リリース)

針・ジャンプ・色替えの追加と削除

開発中・未リリース。DST取込後の命令編集に、選択位置への挿入と削除を追加した。同じ位置へ針落ち・ジャンプ・色替えを追加し、X/Yで位置を調整する。終了命令と最後の針落ちは削除できない。

命令置換も含めて共通の編集経路にまとめ、ロック・グループ・点数/針数・終端を検証してから保存正本と履歴を更新する。不正な操作で変更を部分適用しない。Undo/Redoと保存再編集に対応する。

全272件(Core168/App104)成功、失敗・スキップ・実行時警告なし。独立レビュー指摘なし。手動・実機の確認は最終工程。PES/JEF、糸ブロック管理、生成針の編集変換は残る。アプリにお知らせ機能はなく、未リリースの知見として記録する。

編集正本を保持する複数区間ベジェ曲線

feature開発中(未リリース)

編集正本を保持する複数区間ベジェ曲線

開発中・未リリース。単一区間に限定していた曲線編集を、連続する複数区間へ拡張した。区間選択、形を保つ分割、終点からの延長、開閉、制御点の数値編集とドラッグを追加した。ランニング・ビーンの刺しゅう経路を生成できる。

何を変えたか

schema18で各区間の始点・制御点2個・終点を保存する。表示・針生成用の点列と制御点の正本を区別し、移動・回転・拡縮・複製後も曲線として編集できる。旧単一区間データはそのまま読み込み、編集して保存できる。

区間分割はde Casteljauで制御点を求める。延長は指定変位の直線区間を追加し、後から曲げられる。閉じる場合は終点と始点への区間を追加し、開く場合は形を保持して継ぎ目の拘束を解除する。

なぜ変えたか

複雑な輪郭を少数の制御点で調整できるようにするため。点列化だけでは曲線の編集意図が失われるので、平坦化は表示・出力用に限定する。利用者が明示的に点列へ変換した場合もUndoで戻せる。

技術的な学び

接続点は隣り合う2区間で同じ座標を持つ。移動時は両区間の端点と隣接ハンドルを平行移動する。数値入力では、変更していないハンドル欄をそのまま上書きすると追従を取り消してしまう。独立レビューで発見し、元の値と比較して明示変更されたハンドルだけを優先する方式へ修正した。

閉じた1区間では始点と終点が同じアンカーである。別々に移動すると一方の変更を取り消すため、入力を単一アンカーへ統合し、矛盾した始終点の同時入力は文書を変更せず拒否する。

最大1,024区間。平坦化点だけでなく正本の制御点も文書全体の10万点上限へ計上する。Undo後に存在しない区間番号が残らないよう、選択の正規化を画面からViewModelへ移した。

検証・残り

最終自動テスト全366件成功、失敗・スキップ・実行時警告0。保存・Undo・出力、旧schema6/17互換、閉路・上限・変形・複製と数値編集の回帰を確認し、修正後の独立レビューで追加指摘なし。

手動・Pencil・Files・ミシン受入は最終工程。SVG入力の曲線は従来どおり点列化する。曲線正本のまま面を生成する機能は今回の対象ではなく、面への変更には明示的な点列変換を使う。アプリ内お知らせ機能は現時点でなく、未リリース機能として開発記録に残す。

太さと傾きを再編集できる文字刺しゅう

feature開発中(未リリース)

太さと傾きを再編集できる文字刺しゅう

開発中・未リリース。同梱Noto Sans JPの太さ100〜900と字形の傾き-30〜30度を指定できるようにした。標準400・太字700、直立0度・斜体12度のプリセットも用意した。

目的と変更

文字の見た目を調整した後も、輪郭へ変換せず文字列・サイズ・字間から再編集できるようにする。schema20でoptionalのスタイル情報を保存し、旧形式のnilは標準400・傾き0度として扱う。アーチ配置、回転・拡縮、Undo、保存、ランニング・ビーン・タタミの生成と出力へ接続した。

太字は輪郭を一律に膨らませる方式ではなく、可変フォントのwght軸から字形を生成する。斜体は別のイタリック書体ではなく、基線を固定したせん断変換である。字形の太さ→傾き→アーチ配置→文書変形→平坦化の順で適用する。フォントファイル自体は変更していない。

ハマったこと・学んだこと

太さ100の境界テストで、CoreTextの輪郭抽出がmacOSとiOSシミュレータの両方でクラッシュした。スタックはTrueType字形データの取得からCTFontCreatePathForGlyphへ続いており、太字700では再現しなかった。

フォント生成ヘルパーからCTFontだけを返す形では、元のCGFontとCGDataProviderの寿命が輪郭抽出まで保証されていなかった。両者も返し、抽出処理が終わるまでwithExtendedLifetimeで保持したところ、同じ境界テストが成功した。通常設定の成功だけでは見つからないため、可変フォントの最小・最大値を実際に描画するテストが有用だった。

検証と残り

太字の黒面積増加、斜体の座標変換、上限・下限、非有限値拒否、旧形式、アーチ併用、変形後の文字再編集、保存・Undoと出力を検証する。専用イタリック書体、別フォントの選択、文字ペアのKerning、縦書きは別の残タスク。手動操作とミシン受入は最終工程に残す。アプリ内お知らせ機能はなく、未リリースの開発記録として扱う。

参考

最終自動テスト全379件成功、失敗・スキップ・実行時警告0。独立レビューと寿命修正後の再レビューで追加指摘なし。strict整形検査も成功した。

JEFの読み込みと停止・パレット番号の保持

feature開発中(未リリース)

JEFの読み込みと停止・パレット番号の保持

開発中・未リリース。FilesからJEFを読み込み、針・ジャンプ・色替え・パレット番号0による停止を編集正本へ保持する処理を追加した。保存形式14で保存・Undo/Redo・再編集できる。表示は現在単色で、JEFパレット番号を文字で確認できる。これはメーカーの商品番号とは異なる。

色替えを挿入すると新しいブロックの番号は未設定、削除するとそのブロックの番号も除去する。位置編集や変形では番号を保持する。停止は色ブロックを進めない。停止を色替えへ変えてしまわないよう、DST/EXPへの出力は明示的に拒否する。

検証と知見

全279件(Core174/App105)成功、失敗・スキップ・実行時警告なし。独立pyembroidery 1.5.1 writerで生成した独自の非対称データと、針位置・ジャンプ・色替え・停止を照合した。オフセットや色表、未知命令、切断、入力サイズ/針数上限、取消、保存形式移行、編集後の番号整合も検証した。独立レビュー指摘なし。

公開形式を照合した独自実装で、他社コード・素材を取り込んでいない。入力は2MiB、色枠1000まで。JEF+の拡張データは未対応。色替え付随移動は記録を保持するが、機種別の解釈は最終確認に残している。

JEF書き出し・パレットのRGB表示・PES対応は次の作業。手動操作・実機縫製は未実施で、製品全体の完成ではない。アプリにお知らせ機能はなく、未リリースの知見として記録する。

JEF書き出しで停止とパレット番号を保持

feature開発中(未リリース)

JEF書き出しで停止とパレット番号を保持

開発中・未リリース。書き出し形式にJEFを追加した。取込済みのJEFパレット番号を保持し、通常図形と番号未設定の色ブロックはRGBから標準78色へ近似する。パレット番号はメーカーの商品番号ではなく、実物の糸色を保証しない。

生成針と同じ走査で色ブロックを構築する。取込データの仮表示色と隣接図形の色が同じでも、取込の境界を残す。これにより取込パレットと命令の対応を崩さない。停止は通常色と別の0スロットで保持し、連続停止も削除しない。DST/EXPは引き続き停止を含む出力を拒否する。

形式と互換性の境界

長いジャンプを分割し、針落ちは追加しない。各軸12.7mmを超える針目はエラーとする。指定枠が収まる標準枠を50×50、110×110、126×110、140×200、200×200mmから選び、対象外の枠を拒否する。非対称のデザイン位置を枠中央からの範囲として記録する。

糸切りはゼロ移動ジャンプ3回の慣習を使う。独立readerで解釈を確認したが、ミシンごとの互換性を保証するものではない。手動/Pencil/Files/実機縫製は最終工程に残す。

検証と知見

全285件の自動試験が成功(Core180/App105)。失敗・スキップ・実行時警告なし。自作の非対称fixtureをpyembroidery 1.5.1で読み、3針の座標・2連続停止・色替え・赤/白のRGB・糸切り慣習を照合した。ヘッダの範囲・レコード数も検査した。独立レビューで重大な指摘なし。

処理は独自実装。RGBの数値表のみMITのpyembroidery 1.5.1から利用し、出典とライセンスをアプリのCoreリソースに同梱した。依存ライブラリ本体や競合の素材・ソースコードは取り込んでいない。

JEF+、パレット色のプレビュー表示、PES入出力などは残る。製品全体の完成ではない。アプリ内のお知らせ機能はなく、未リリースの開発記録として公開する。

取込針データの糸表を編集する

feature開発中(未リリース)

取込針データの糸表を編集する

開発中・未リリース。取り込んだ針データと、通常図案から針へ変換したデータの色ブロックに、ライブラリの糸を割り当てる機能を追加した。メーカー・番号・RGBを編集しながら針命令列を保持する。

色ブロックは色替え命令でのみ進む。Stopは同じ糸の停止なので、ブロックを増やさない。割り当ては糸表の該当スロットのみ置換し、針・Jump・Trim・Stop・色替えの位置と原点は変えない。未知の他スロットはそのまま残す。

JEF由来データは元のパレット番号が優先されるため、新しいRGBを登録するだけでは古い色で出力される。変更したスロットの古いJEF番号をnilにし、生成時に新RGBから再近似するようにした。未変更の番号は保持する。PESへは新しいメタデータを渡す。

管理画面では色ブロック番号と現在の糸情報を表示する。選択変更や色替え削除で番号が範囲外にならないよう補正し、類似色検索も対象スロットの色を基準にする。Coreで命令保持・番号解除・無効スロット・保存Undoを確認し、Appで割り当てからPES往復まで検証した。

手動・実機の受入は最終工程。PESの同一糸色替え等の形式制限はそのまま適用される。

最終全352件成功。失敗・スキップ・実行時警告0、独立レビュー指摘なし。

生成針も確認して図案を刺しゅう枠に合わせる

feature開発中(未リリース)

生成針も確認して図案を刺しゅう枠に合わせる

開発中・未リリース。表示中の図案を比率保持で拡縮し、指定余白を残して枠の中央に置く操作を追加した。標準では縮小のみ、小さい図案の拡大は明示的に選ぶ。編集可能な輪郭・文字・クロスマス・取込針と糸情報を維持する。

輪郭だけでは足りない

サテンの引きつれ補正は輪郭より外側へ針を置く。そのため、最初に正本の境界から変換を求めた後、ステッチを再生成して針とジャンプ位置も検査する。必要なら縮小と中心合わせを繰り返す。設定した補正量や密度は変えず、収まらなければエラーとして元の状態を維持する。

処理は文書のスナップショットに対して実行し、成功後に一度だけUndo履歴へ追加する。非表示図案と下絵は対象外。表示中のロックや一部だけ非表示のグループは、意図しない部分変形を避けるため拒否する。画面上では取消でき、処理中の編集や文書切替で古くなった結果は適用しない。

検証と残り

Coreの8件で比率・中心・拡大許可・余白・Undo/Redo・ロック・非表示グループ・クロス正本保持・サテン補正後の針境界を確認した。Appでは保存往復・Undo/Redo・取消・文書切替を確認。独立レビューで補正付きサテンの収束問題を発見した。固定割合の縮小では枠内の空きが狭いと打ち切りになるため、生成境界と正本境界の差を補正分として差し引く縮小率に変更。余白45mm・47.5mmで失敗を再現してから修正し、再レビューで追加の重大な指摘なし。

取込針の拡縮は針間隔も変えるため、枠内に収まっても縫い品質を保証するものではない。機種別枠・Multi Hoopは残項目。手動操作・Pencil・ミシンでの受入は最終工程とし、この記録はリリース告知ではない。

最終の全体回帰は340件成功。失敗・スキップ・実行時警告は0。

文字列を再編集できるタタミ文字刺しゅう

feature開発中(未リリース)

文字列を再編集できるタタミ文字刺しゅう

開発中・未リリース。手動操作・実機・ミシン縫製は最終工程で確認する。

変更と利用者への効果

Noto Sans JPの文字を、輪郭のランニング/ビーンに加えてタタミで塗りつぶせるようにした。追加後のステッチ種類から変更し、行間隔・角度・最大3層下縫い・留め・終了後糸切りを設定できる。文字列の再入力や変形後も設定を保持する。

設計と知見

文字の各輪郭を独立した面として塗ると、Oや日の穴も埋まってしまう。全輪郭の交点を走査線ごとに整列し、向き付きの交差数がゼロでない区間を縫うnonzero windingを採用した。同方向の重なりは一体の塗りとして扱い、離れた部首・穴の間はジャンプする。通常面の偶奇規則と厳格な穴検証は維持した。

上縫いと下縫いで針数10万、走査仕事量200万を共有する。保存形式を11へ上げ、旧形式1〜10を移行可能にし、旧形式へ新しい組合せを混入した入力は拒否する。「輪郭を個別の線に変換」では塗りを解除してrunningへ変えることを画面に明記した。

検証

249件(Core151 + App98)成功、失敗・スキップ・実行時警告なし。独立レビュー指摘なし。

穴・島・重なり・反転を人工形状で検証し、実フォントの針区間中点が塗り領域内にあることをCore Graphicsの別実装で照合した。予算ちょうどと超過、保存往復、再編集、Undo/Redo、ロック、取消、DST/EXPも確認した。

残項目

サテン文字・曲線配置・モノグラム・名前の一括生成は未実装。経路最適化は未対応で、行間のジャンプが多い。縫製品質はまだ確認していない。アプリにお知らせ機能はなく、未リリースの知見として記録する。

穴を横切らず面の渡り糸を減らす

feature開発中(未リリース)

穴を横切らず面の渡り糸を減らす

開発中・未リリース。面とタタミ文字に「安全な短い区間を縫いつなぐ」設定を追加した。既定は無効で、既存作品の生成結果を維持する。

設計と知見

始点・終点や少数のサンプルだけで面内を判定すると、細い穴を見逃す。接続線と全輪郭の交点で区間を分割し、各区間の中点を偶奇規則または文字の非ゼロ規則で判定する。最大針目長以内で全区間が面内か境界上の場合だけ接続する。浮動小数点で中点を区別できない区間は拒否する。

判定仕事量は全下縫い・上縫いで200万を共有し、上限到達時はジャンプへ戻す。各下縫い層と上縫いの間は結合しない。生成と針数計算は同じ判定経路を通す。設定は保存形式12で保持し、旧1〜11を移行できる。

検証

全254テスト成功、失敗・スキップ・実行時警告なし。矩形のrun削減、凹部・極細穴、実フォント、下縫い層、予算、針数一致、保存、Undo/Redo、DST/EXPを検証した。独立レビューで具体的な不具合なし。

残項目

境界を迂回する経路探索や、オブジェクト順・糸色を含む全体最適化は未実装。実機での渡り糸・縫い品質は最終工程で確認する。アプリにお知らせ機能はなく、開発中の記録として公開する。

DSTを取り込み、針位置を編集して保存・再出力する

feature開発中(未リリース)

DSTを取り込み、針位置を編集して保存・再出力する

開発中・未リリース。FilesからDSTを読み込み、命令番号を選んで針位置を調整、針落ちとジャンプを切り替えられるようにした。元の機械原点を枠中央に配置し、自動縮小しない。編集後の移動・回転・拡縮・複製、Undo/Redo、保存再編集、DST/EXP再出力に対応する。

設計と知見

読み込んだ針を通常の輪郭に平坦化すると、ジャンプや重複針、色替えの順序が失われる。命令列を独立した正本として保存形式13で保持し、画面と出力へ接続した。色替えに座標移動が付いている場合も、針を追加せず保持する。連続ジャンプから糸切りは推測しない。未対応のスパンコール命令は明示的に拒否する。

生成プレビューだけに表示を依存させると、プレビューOFFや枠外エラー時に図案が消えて位置を修正できない。独立レビューで発見し、保存命令からも針・ジャンプを区別して描画するよう修正した。

検証と残り

全270件(Core167/App103)成功、失敗・スキップ・実行時警告なし。形式の既知バイト列、全軸移動値、独立pyembroidery 1.5.1 writerの独自データ、保存往復、針編集、非同期取消・世代不一致・ロック・有界ファイル読み込みを検証した。公開形式を照合して独自実装し、他社コード・素材を取り込んでいない。

PES/JEF、拡張糸色・色ブロック編集、針命令の追加削除、生成針からの変換は残る。手動操作・実機互換・縫製は最終確認待ち。アプリにお知らせ機能はなく、未リリースの開発知見として記録する。

画像から編集可能なクロスステッチを生成する

feature開発中(未リリース)

画像から編集可能なクロスステッチを生成する

開発中・未リリース。PNG/JPEGから減色・背景除去・小領域除去を経て、クロスステッチのマスを生成する機能を追加した。マスの糸色・空白・寸法を編集し、保存・再編集・PES/DST/JEF/EXP出力へ接続した。

編集正本と生成針を分ける

各マスを最初から針列へ変換すると、色の塗り替えやマス寸法の変更が難しくなる。schema16では行列数、原点、2本の基底ベクトル、パレット、各マスの色番号または空白を保存する。移動・回転・拡縮は原点と基底へ作用し、マスを保持する。出力時だけ、色ごとに行を往復して2対角線・4針へ展開する。未使用色は出力せず、隣接オブジェクトと同色の場合は余分な色替えを作らない。

画像の実寸と指定マス寸法から行列数を切り上げる。端の部分マスで右下へ最大1マス未満広がるため、元画像を勝手に縮めず生成後の枠も検査する。最大256行×256列、アクティブ25,000マス。透明・背景除去後の離れた多数のマスは、タタミのオブジェクト数制限と分けて扱う。

操作と検証

配置図のタップまたは行・列の指定で対象マスを選び、色を割り当てるか空白化する。パレット編集は同じ色番号の全マスに反映する。Undo/Redoと保存往復を維持する。画像変換はバックグラウンドで実行し、取消や編集後に古い結果を適用しない。

クロス生成と画像統合の12件は成功。全体回帰330件の初回は保存形式のテスト期待値1件に更新漏れがあり、修正後の当該試験と、最終全330件が成功した。失敗・スキップ・実行時警告は0。独立レビューで重大な不具合の指摘はなかった。手動操作・Pencil・実機縫製は最終工程に残す。

残っていること

ハーフクロス、反復回数指定、クロス専用の縫い経路最適化は未対応。対角線間にもジャンプがあるため、実際の糸渡りや縫い品質は実機確認が必要。Photo/Paint/Sketch等も別の未実装項目で、競合製品全体との同等性を達成したという意味ではない。アプリ内お知らせは未リリースのため追加しない。

生成した刺しゅうを1針単位で編集する

feature開発中(未リリース)

生成した刺しゅうを1針単位で編集する

開発中・未リリース。表示中のデザインを1つの編集用針データへ変換する機能を追加した。ランニング・ビーン・サテン・タタミ・曲線・文字から生成した針と、取り込んだ刺しゅうをまとめ、縫い順・色替え・停止・糸切り・糸情報を保つ。変換後は既存の命令編集画面で針の座標変更、針とジャンプの切替、追加・削除、保存・再編集・出力ができる。

明示的な変換と元の図案

普段の生成や保存では輪郭を針へ平坦化しない。変換を明示的に確定した場合だけ、表示図案を置き換える。変換後は面密度や文字内容から再生成できないため、元図案を残す場合はプロジェクトを先に複製する。Undo一回で戻せるが、Undo履歴は保存されず、再オープン後は元の輪郭へ復元できないことを画面で説明する。

非表示の図案、下絵、枠、レイヤーを保持する。ロックされた表示図案、表示と非表示にまたがるグループ、枠外、保存点数上限、取消では変更を適用しない。変換をバックグラウンドで実行し、編集中や画面離脱後に古い結果を適用しない。

検証と残る範囲

全309件(Core200/App109)の自動試験成功、失敗・スキップ・実行時警告なし。Core6件とApp3件を追加し、複数色/停止/糸切り/糸表、穴付きタタミ/下縫い/留め縫い、サテン/ビーン/曲線/文字/グループの変換前後を照合した。保存往復、Undo/Redo、針編集後のPES/DST/JEF/EXP出力、取消と古い結果の拒否も確認した。

レビューで見つかった画面離脱時の取消漏れは修正済み。現在の対象は表示デザイン全体で、選択部分だけの変換は残項目。手動・Pencil・ミシン受入も最終工程に残している。アプリ内お知らせ機能はなく、未リリースの開発記録として掲載する。

文字の正本を保持したアーチ刺しゅう

feature開発中(未リリース)

文字の正本を保持したアーチ刺しゅう

開発中・未リリース。文字刺しゅうに上アーチ・下アーチと半径指定を追加した。文字列・サイズ・字間・配置設定を保存し、回転・拡縮後も再編集できる。

変更と目的

直線だけだった文字配置を円弧へ拡張した。半径は2〜10000mm。字形をゆがめず、各グリフを円弧の接線方向へ回転する。文字列全体の基線中央は直線時と同じ位置に保つ。利用者はランニング・ビーン・タタミを選び、プロジェクト保存と刺しゅうデータ出力へ進める。

文字列が一周を超える場合は、半径・文字サイズ・字間の調整を促す。自動で一周へ等間隔配分する機能や、スパイラル・任意パス・Warpは別の未実装機能として管理する。

技術的な内容

CoreTextから文字列の実幅、各グリフの位置と送り幅を取得する。送り幅の中央を円弧へ配置し、輪郭の制御点を同じ角度で回転する。最後に文書の原点・基底ベクトルを適用してから0.05mm精度で平坦化する。配置と字形の変換を分離することで、文書上の非等方拡縮も保持できる。

schema19でoptionalのアーチ情報を追加し、旧データのnilは直線として扱う。CoreTextのレイアウトオブジェクトは生成ジョブの中で扱い、スレッド間共有を行わない。

学んだこと

直線配置への切替でoptional設定をnilにすると、再びアーチに戻した際に入力済みの半径を失う。文書正本とは別にフォームの半径を記憶し、切替中の入力を保持した。

一周超過を生成時だけ検証すると、半径を改変した保存データを読み込んだ後で再編集が失敗する。生成と文書検証で同じCoreTextレイアウト生成処理を共有し、実幅と円周を比較するよう修正した。独立レビューでこれらを確認し、修正後の再レビューでは追加指摘なし。

検証・残り

配置の左右対称性、字形の剛体保持、上/下の向き、半径異常、一周超過、旧形式互換、保存・Undo、空白と字間、非等方拡縮、文字再編集、タタミ生成と全出力形式を自動試験で確認する。手動・Pencil・Files・ミシンの受入は最終工程。アプリ内お知らせ機能はなく、未リリースの開発記録として残す。

参照資料

最終自動テスト全373件成功。失敗・スキップ・実行時警告0。strict整形検査も成功した。

図案終了後の糸切り指定

feature開発中(未リリース)

図案終了後の糸切り指定

開発中・未リリース。実機の糸切り動作は未確認。

何を変えたか

全ステッチ種別で図案終了後の糸切りを指定し、保存・Undoに対応した。針数は変えず、命令として生成する。schema8で保持しv1〜v7を移行する。

なぜ変えたか / 利用者への効果

別の図案へ移る前や最後に糸切りを指定できるようにするため。留め縫いとは独立に指定する。

技術的な内容

EXPは専用命令、DSTは公開実装で使われる3回の微小ジャンプを使用する。DSTの中間位置も枠内か検査し、ヘッダーの範囲へ反映する。途中で枠外へ出る場合は拒否する。位置が変わらないEXPにはこのDST固有の制限を適用しない。

判断と学び

糸切りを針として扱うと針数や再生がずれるため、正本設定と生成命令を分けた。形式ごとに糸切り表現と必要な命令数が異なるため、出力後の予算も個別に検証する必要があった。

自動検証と制限

合同193件成功(Core110件を含む)。失敗・スキップ・実行時警告なし。別readerで9針と2回の糸切り位置を照合。DSTはtrim_at=3での確認であり、全機種の動作を保証しない。機種側設定と試縫いは最終工程へ残す。

EXP書き出しを追加

feature開発中(未リリース)

EXP書き出しを追加

開発中・未リリース。実機互換性とFilesの手動操作は未確認。

何を変えたか

書き出しメニューでDSTとEXPを選べるようにした。針位置・ジャンプ・色替えを保ち、選んだ形式に対応するファイル名とバイト列を確定時に保持する。編集正本と文書形式は変更しない。

なぜ変えたか / 利用者への効果

DST以外の機械向け出力を増やすため。編集可能な正本から同じ図案を複数形式へ出力できる。

技術的な内容

枠中心からの0.1mm単位の座標をEXPの相対移動へ変換する。長いジャンプだけ分割し針落ちは増やさない。EXPの命令仕様はMITライセンスの公開実装で照合し、Swiftで実装した。コード転載や新規ランタイム依存はない。

判断と学び

形式を増やすと枠外・丸め・上限検査に差が生じやすい。第一段階では既存DSTエンコーダで共通安全条件を検証してからEXPへ変換し、一時データ生成のコストを受け入れた。EXP本来の移動上限127に対し既存の121を維持している。

検証と制限

合同185件成功(Core105件を含む)。失敗・スキップ・実行時警告なし。別ライブラリで留め付き2色図案のDST/EXPを読み、17針・XY方向・色替え位置・長いジャンプを照合した。実機では未検証。EXP読込や付随画像/糸色ファイル、明示的な糸切り命令は未対応。RGB糸色と編集輪郭はEXPに保持しない。

一次照合元

pyembroidery ExpWriter、MIT License。

日本語・英字の輪郭文字を再編集できるようにした

feature開発中(未リリース)

日本語・英字の輪郭文字を再編集できるようにした

開発中・未リリース。実機操作・ミシン縫製は最終工程で確認する。

何を変えたか

Noto Sans JPをOFL全文とともに同梱し、文字の輪郭をランニング/ビーンで生成する機能を追加した。文字列・文字サイズ・字間・基準位置を編集し、移動・回転・拡縮・反転後も文字列から再編集できる。対応しない文字は通知して追加を拒否する。

なぜ変えたか / 利用者への効果

名前や短い日本語を描き直さずに作成・修正するため。文字列を通常の線へ変換する操作も用意し、細かな輪郭編集へ移れるようにした。変換はUndoで戻せる。

技術的な内容

CoreTextで字形を取得し、曲線を点列へ近似する。編集正本には文字列・配置のアフィン基底・輪郭キャッシュを保存する。読込時には再描画せず、OS側の描画変化によって保存済み図案を変えない。明示的な文字更新時に再生成する。文書形式はschema9へ更新した。

一文字の穴や複数文字を1本の線へ連結すると不要な縫い線ができるため、輪郭ごとに生成し、間はジャンプする。留め縫いは文字オブジェクト全体の開始・終了、糸切りは全輪郭後に適用する。個別線への変換でもこの配置を保つ。

判断と学び

文字の位置だけを保存すると回転・反転後の再編集で向きが戻る。基準位置に加えて2本の基底ベクトルも変形し、再生成に引き継いだ。また、境界検査・選択サイズ・プレビュー・点数予算を最初の輪郭だけで計算しないよう、全輪郭の共通参照を導入した。

自動検証と残項目

合同206件成功(Core116 + App90)、失敗・スキップ・実行時警告なし。英日文字、欠字、保存、Undo、変形後の再編集、輪郭変換、非同期取消・遅延結果拒否、後方文字の枠外検出、DST/EXP出力を確認した。独立コードレビューに修正必須指摘なし。

今回の対応は輪郭文字のみ。塗りつぶし文字・サテン文字・曲線配置・モノグラム・差し込み生成は未実装。小さい文字の試縫いを含む物理的な品質は未確認。

サテン・面にも開始と終了の留め縫いを追加

feature開発中(未リリース)

サテン・面にも開始と終了の留め縫いを追加

開発中・未リリース。実機・ミシンでの確認は最終工程に残す。

何を変えたか

ランニング/ビーンで提供していた留め縫いを、サテンとタタミ面にも追加した。開始・終了を個別に指定し、長さは0.1〜2mm。端の針目を超えない範囲で2往復し、各端に4針を追加する。設定は保存・Undoに対応する。

なぜ変えたか / 利用者への効果

線だけでなく面やサテンにも、生成した針経路に沿った留めを指定できるようにするため。種類を変更しても設定を保持し、手動針へ変更した場合だけ外す。

技術的な内容

穴や下縫いにより図案が複数の連続経路へ分かれていても、最初と最後の経路の端だけに追加する。中間の行や輪郭には追加せず、ジャンプの位置を保つ。糸切りは終了留めの後に配置する。文書形式はschema10へ更新し、旧版の有効な文書を移行する。

判断と学び

針生成側だけで追加すると、保存前の検証キャッシュが古い針数を使って上限を見逃す。面生成の共通処理で数と針経路の双方へ反映し、検証経路をそろえた。留めの点は既存の端の線分上に収めるため、穴をまたぐ新しい縫い線を作らない。

検証と残項目

合同210件成功(Core119 + App91)、失敗・スキップ・実行時警告なし。サテン下縫い、穴と複数下縫いの面、針数上限、保存、Undo、ロック、DST/EXP出力を確認した。99996針に開始留めを加えて100000針、さらに終了留めを加えると文書を変えず拒否するテストを含む。

試縫いによる留めの効果は未確認。糸パレット、経路最適化、文字の塗りつぶし・サテン化などは引き続き未実装。

針順の自動再生

feature開発中(未リリース)

針順の自動再生

開発中・未リリース。実機確認と試縫いは未実施。

何を変えたか

針位置スライダーに再生/一時停止、先頭、5/20/50針毎秒を追加した。全経路を表示したまま赤い針位置が縫い順に移動する。終端で停止し、もう一度再生すると先頭から進む。

なぜ変えたか / 利用者への効果

スライダーを動かし続けずに縫い順を確認できるようにするため。スライダー操作、編集、再生成、プレビュー非表示、画面離脱やバックグラウンド移行では停止する。

技術的な内容

再生状態と時計を分離し、経過秒から針数の小数進行を保持する。Taskは待機後にキャンセルを再確認してから状態へ反映する。針位置の観測をMarkerとControlsに限定し、静的Canvasから動くマーカーを分離した。正本・保存・DSTデータは変更しない。

判断と学び

各tickの整数丸めだけでは低速再生が進まないため、小数分を持ち越す。大量の針経路を毎tick構築し直す必要がないよう、変化する針位置だけを別Viewにした。

自動検証と制限

合同168件成功(Core93件を含む)。失敗・スキップ・実行時警告なし。状態遷移、終端、シーク、空/1針、不正時間、巨大時間を自動確認。ライフサイクル処理と描画の分離はコードレビューで確認したが、実機操作と性能測定は未実施。機械の動作時間・糸切り待ち時間は再現しない。

ラインの開始・終了留め縫い

feature開発中(未リリース)

ラインの開始・終了留め縫い

開発中・未リリース。実機での糸止め効果と試縫いは未確認。

何を変えたか

ランニング/ビーンの縫い始め・縫い終わりへ各4針の短い往復を追加できるようにした。ON/OFFと長さ0.1〜2mmを設定し、保存・Undoに対応する。点列・閉輪郭・1区間ベジェに適用する。

なぜ変えたか / 利用者への効果

端の留めを手動で針落ち入力せずに指定するため。設定は編集可能な正本に残し、針生成時に追加するので元の輪郭を変えずに解除できる。

技術的な内容

ランニング/ビーンの通常生成後に一度だけ適用する。各端の隣の異なる針へ向かい、指定長かその針間隔の短い方で2往復する。追加針を事前の針数検証と全体10万針上限にも含めた。閉輪郭も始終点それぞれの方向で留める。

schema7で設定を保持し、v1〜v6は既存出力を維持したまま移行する。ロック・グループ内の単体設定は拒否。分割では双方に設定を継承し、結合では同じ設定のみ許可して外側の端へ集約する。

判断と学び

ビーンの各往復へ留めを適用すると想定外に増えるため、最終点列へ一度だけ追加する。針生成だけ増やして検証側の予算を更新しないと、編集時の上限と書き出し結果が一致しない。端の針目上へ収めることで、輪郭外へ留めを延長しない設計にした。

対応範囲

面・サテン・手動針の留めと糸切り命令は未対応。非対応種別への変更は設定を外しUndoで復元する。実機とミシンの動作確認は最終工程へ残す。

自動検証

合同176件成功(Core98件を含む)、失敗・スキップ・実行時警告なし。独立readerでも留め開始4針・本縫い5針・留め終了4針の順番と座標を照合した。

ベジェ制御点ドラッグとライン輪郭の開閉

feature開発中(未リリース)

ベジェ制御点ドラッグとライン輪郭の開閉

開発中・未リリース。実機操作と試縫いは最終工程へ残す。

何を変えたか

ベジェの始点・制御点2つ・終点を選択してキャンバス上のハンドルで移動できるようにした。既存の数値編集も維持する。ランニング/ビーンの点列では輪郭を開く・閉じる操作を追加し、保存とUndoへ接続した。

なぜ変えたか / 利用者への効果

制御点を数値だけで調整する手間を減らし、図案上で移動できるようにするため。輪郭を閉じる場合も点列を作り直す必要がなくなる。

技術的な内容

ドラッグ開始時の座標変換、対象ID、制御点番号、入力世代を保持する。移動中はハンドルだけを表示更新し、終了時に正本を一度だけ変更する。選択・モード・制御点・編集内容が変わった後の遅延イベントは受理しない。

閉じたラインは終点から始点の帰路を生成する。閉じるには異なる3点が必要。開閉で元の点列は削除しない。ロック、グループ内、曲線正本、未対応の縫い種別は拒否し、針数上限超過時も元の文書を保つ。

判断と学び

ドラッグ中の各イベントを履歴へ入れるとUndoが細切れになるため、表示の一時状態と確定状態を分けた。開始位置からの移動量を使うことで、ハンドル中心以外を触れた場合の位置飛びを避ける。

検証・制限

Core93件、iPad合同165件成功(Coreを含む)。失敗・スキップ・実行時警告なし。実機Pencil操作やミシン試縫いは未確認。ベジェは引き続き1区間のみ。輪郭を開いても、元から始点と同位置の終点を持つ場合はその点を維持する。

制御点を残したベジェ曲線の刺しゅう編集

feature開発中(未リリース)

制御点を残したベジェ曲線の刺しゅう編集

開発中・未リリース。実機操作とミシン確認は最終工程へ残す。

何を変えたか

始点・終点・2つの制御点を持つ1区間の3次ベジェ曲線を追加し、数値編集・保存・Undoとランニング/ビーン生成へ接続する。通常の節点編集を行う場合は、明示的に点列へ変換する。

なぜ変えたか / 利用者への効果

滑らかな線の形を、多数の節点ではなく少数の制御点で調整できるようにするため。保存や変形後も制御点から再編集できる構成にした。

技術的な内容

制御点が編集正本で、針生成に使う点列は派生キャッシュとして保持する。de Casteljau分割で制御点から端点線分までの距離を検査し、固定許容誤差0.05mmで近似する。点数・再帰深さ・座標範囲・取消を制限する。変形時は制御点を変形して再近似し、保存時には正本とキャッシュの一致を検証する。

判断と学び

既存の点列だけを変形すると、曲線正本と生成データが別の形になってしまう。曲線のまま使える編集経路と、点列化が必要な編集経路を明確に分ける必要があった。文書形式v6で古いアプリが曲線情報を黙って落とすことを防ぐ。

制限

1区間、ランニング/ビーンと数値編集が対象。複数区間、SVGの曲線読込、Pencilでの制御点ドラッグは後続。面/サテン等に使う場合は点列へ変換する。実機の見た目と試縫いは未検証。

自動検証

Core91件、iPad合同160件成功(Coreを含む)。近似精度・整合性・変形・上限・保存・Undo・DST出力を確認。失敗・スキップ・実行時警告なし。実機操作と試縫いは未確認。

開いた刺しゅう線の分割・結合

feature開発中(未リリース)

開いた刺しゅう線の分割・結合

開発中・未リリース。実機操作・ミシン確認は最終工程へ残す。

何を変えたか

開いたランニング/ビーン線を内部節点で2本に分割し、縫い順で隣接する同設定の2本を結合できるようにした。正本の点列を編集し、保存・Undo・再生成につなぐ。

なぜ変えたか / 利用者への効果

描いた線の一部を別々に調整したり、分かれている輪郭線を1本へまとめたりする基本編集を支えるため。

技術的な内容と判断

分割では前半のIDを維持し、新しい後半を直後に配置する。結合では縫い順で先の線の終点から後の始点へつなぐ。同一点の接続部だけ重複除去する。端点間が離れる場合も縫うことをUIに明記し、自動反転は行わない。

結合は種類・針目長・色・レイヤー・表示が同じ場合に限定。非隣接の線を結合すると途中の図案との縫い順が変わるため拒否する。グループ・ロック・閉輪郭・面・サテン・手動針も対象外。失敗時は正本と履歴を変更しない。

学んだこと

幾何的につながればよいだけでなく、縫い順と編集メタデータを保持する条件が必要だった。生成針を直接つなぐのではなく正本を操作し、既存の針数上限と再生成へ戻す設計にした。

制限

ベジェ曲線、面のブーリアン演算は未実装。分割・結合によって再生成時の針位置は変わり得る。

自動検証

Core86件、iPad合同153件成功(Core含む)。設定・ID・順序・ロック・上限・Undo・保存とApp選択状態を検証。実機操作・試縫いは未確認。

面の下縫いを最大3層へ拡張

feature開発中(未リリース)

面の下縫いを最大3層へ拡張

開発中・未リリース。手動操作とミシンの試縫いは最終工程に残す。

何を変えたか

既存の下縫いに最大2層を追加し、各層の角度・行間隔・針目長を独立設定する。追加した層の削除、全体の無効化、Undo/Redo、文書保存に対応。第一層・追加層の順に下縫いを行ってから上縫いを生成する。

なぜ変えたか / 利用者への効果

複数の方向で下縫いを組み合わせる設定を、面の編集正本に保持するため。層を別オブジェクトへ複製せず管理できる。実際の生地での効果は未検証。

技術的な内容

既存の第一層を維持し、追加層をoptional配列で保存する。文書形式v5を導入し、v1〜v4の読込で既存出力を維持する。第一層なしの追加層と上限超過は拒否。生成時は全層で共通の針数・走査量の予算を使う。

学んだこと

設定の無効化は第一層だけを消すと不整合になる。全層を同一の編集トランザクションで消し、Undoでまとめて戻す必要がある。層を追加した際の既定角度は90度ずらし、利用者が後から独立調整できるようにした。

制限

最大3層。インセットと経路最適化は未実装。実機操作・試縫いは未実施。

検証

Core79件、iPad合同142件成功(Coreを含む)。独立DST readerで下縫い30針×3層→上縫い60針、同色・穴回避・針目長・ジャンプを照合。実機効果は未検証。

刺しゅう枠の数値設定と汎用プリセット

feature開発中(未リリース)

刺しゅう枠の数値設定と汎用プリセット

開発中・未リリース。手動操作とミシン確認は最後に行う。

何を変えたか

枠の幅・高さを各辺10〜1000mmで指定し、汎用サイズ選択・縦横入替も可能にした。変更は単一Undoで取り消せ、既存の保存項目へ保持する。図案・下絵の位置とサイズは変更しない。

なぜ変えたか / 利用者への効果

初期100×100mm以外の制作範囲を指定できるようにするため。枠を縮めて図案が外側になった場合も編集正本を保持し、警告と書出し禁止で修正を促す。汎用寸法は機種別の互換性を保証しない。

技術的な内容と学び

枠の変更を通常の編集トランザクションに統合し、保存・Undo・再生成・入力世代の更新を共有する。未確定の手動針や穴描画中は枠変更を拒否して入力を保護する。寸法変更と図案変形を別の操作として扱うことで、意図しない拡縮を防げる。

検証

Core81件、iPad合同146件成功(Coreを含む)。縮小時の枠外検出、書出し禁止、Undo復元、保存、異常寸法、未確定入力保護を自動テスト。実機操作・試縫いは未検証。

面の刺しゅうへ独立設定のタタミ下縫いを追加

feature開発中(未リリース)

面の刺しゅうへ独立設定のタタミ下縫いを追加

開発中・未リリース。実機Pencil操作とミシン縫製は最終工程で確認する。

何を変えたか

面の上縫いより先に、同じ外周と穴に粗いタタミの下縫いを生成する。行間隔・絶対角度・最大針目長を独立設定でき、有効/無効の切替、保存、Undo/Redoに対応した。

なぜ変えたか / 利用者への効果

面の刺しゅうを支える下縫いを、別オブジェクトへ手作業で複製せず調整できるようにするため。実際の生地での効果は未検証。

技術的な内容

下縫いと上縫いで既存の走査線処理を共有し、単一の出力へ順に追加する。穴や行の間はジャンプ。合計針数10万と走査作業200万回の上限を共有する。下縫い設定をoptionalで保存し、旧文書は無効状態を維持する。旧アプリによる設定の黙示的な消失を防ぐため文書形式をv4に更新した。

学んだこと

各パスが個別に上限を満たしても、二つを合わせると超える場合がある。出力配列だけでなく計数モードでも共通予算を使う必要がある。生成順序とジャンプは画面だけでは確かめにくいため、独立readerでもDSTを照合した。

検証

Core75件、iPad Simulator合同136件成功(Coreを含む)。失敗・スキップ・実行時警告なし。pyembroidery 1.5.1で下縫い30針→上縫い60針、同色、針目長、穴回避、ジャンプを照合。

今後 / 制限

今回は1層。輪郭から内側への補正、複数層、自動経路最適化は未実装。手動操作と糸・生地を使った試縫いは未実施。

SVGの直線輪郭を編集可能な図案へ取り込む(未リリース)

feature開発中(未リリース)

SVGの直線輪郭を編集可能な図案へ取り込む(未リリース)

何を変えたか

Filesから選んだSVGの直線path・多角形・矩形・円・楕円を、黒のランニング刺しゅうへ取り込む第一版を追加した。元の色や塗りを再現するインポーターではなく、輪郭の入力として扱う。曲線・transform・文字・CSS等を含むファイルは全体を拒否する。開発中・未リリース。

なぜ変えたか

手描きや基本図形に加えて外部の輪郭を制作に使い、追加後も節点・糸色・ステッチ種類を調整できるようにするため。

ユーザーにとって何が良くなったか

対応する複数輪郭を現在のレイヤーへまとめて追加し、一回のUndoで戻せる。図案の一部だけが欠ける黙った読み飛ばしは行わず、未対応内容を通知する。

技術的な内容

寸法指定とviewBoxをmmへ変換する。XML入力を2MiBに制限し、DTD・外部実体・外部参照を拒否する。要素・深さ・輪郭・頂点の上限を設ける。解析をバックグラウンドへ移し、完了時に文書・操作世代を再照合する。読み込み中に編集状態が変われば、その結果を文書へ追加しない。

ハマったこと・学んだこと

単にファイルを読めることと、部分欠落なく取り込めることは別の条件である。第一版では対応要素・属性を明示し、非対応を丸ごと拒否する方針を選んだ。見た目のSVGと刺しゅうの輪郭入力を区別し、色や塗りを捨てる仕様を取り込み前に表示する。

レビューで修正した点

pathでは許される数値の省略区切りをviewBoxにも適用してしまい、不正な寸法列を別の値として受け付ける問題を修正した。用途ごとの区切り規則を分け、拒否する例と受け付ける例の両方を回帰テストにした。未確定の手動入力中はSVG取り込みを開始させず、入力中の点を保護する。

自動検証

2026-09-24にCore 65件とiPad合同116件(Coreを含む)が成功。実ファイルから編集輪郭を作り保存復元する試験、世代変更・取消・ロック失敗を確認した。

参考と今後

W3Cの座標系仕様(上記URL、2026-09-23確認)を参照。SVGの曲線・transform・色と面の解釈、画像取り込みは残項目。Pencil実機操作とミシン縫製は最終工程に残しており、取り込み成功は縫製品質の保証ではない。

PNG/JPEGを刺しゅう制作の下絵として保存する

feature開発中(未リリース)

PNG/JPEGを刺しゅう制作の下絵として保存する

開発中・未リリース。実機Pencilとミシンによる確認は最終工程へ残す。

何を変えたか

PNG/JPEGを1文書1枚の下絵として取り込み、位置・比率固定サイズ・透明度・表示を調整できる実装を追加。置換・削除・配置変更はUndo対象で、画像を文書と復旧下書きに保持する。

なぜ変えたか / 利用者への効果

写真やイラストを見ながらPencilで輪郭を描くため。別のアプリを見比べず、下絵に重ねて刺しゅうの編集正本を作れる。自動パンチや写真刺しゅう生成は未実装。

技術的な内容

ImageIOでEXIFの向きを反映し、長辺最大2048pxのPNGへ正規化する。入力はPNG/JPEG・単一フレーム・20MiB・100MP・各辺32768px以内、保存PNGは8MiB以内。これらはアプリ独自の制限。文書全体の20MiB上限も維持する。

画像は編集正本の参照情報として保持し、針生成やDSTへ混入させない。旧アプリが画像を黙って捨てないよう文書形式をv3へ上げ、v1/v2は検証後に移行する。

学んだこと / 判断

PNGの署名とIHDR検査だけでは画像全体の正常性は保証できない。純Swift Coreではサイズとヘッダーを検査し、Appの文書・下書き読込時にはImageIOで完全デコードする。文書置換前に画像を検査し、破損画像によって現在の図案を失わないようにした。

非同期取り込みは世代照合とキャンセルで遅延結果を排除する。容量だけでなく展開後の画素数を制限することも必要だった。

検証

Core 70件、iPad Simulator合同128件成功(Coreを含む)。失敗・スキップ・実行時警告なし。EXIF回転、縮小、保存復元、Undo、破損画像拒否、文書置換の原子性、古い結果の破棄、未確定入力の保護を検証。実機の表示と操作は未確認。

基本図形と節点編集の制作機能を追加(未リリース)

feature開発中(未リリース)

基本図形と節点編集の制作機能を追加(未リリース)

何を変えたか

中心・幅・高さを指定した矩形と楕円近似輪郭の追加、節点の中間追加・削除・輪郭反転を追加した。楕円は8〜256点の閉じた多角形であり、ベジェ曲線ではない。実装段階で未リリース、実機のPencil操作とミシン確認は最終工程に残る。

なぜ変えたか

手描きだけでなく、寸法を指定した図形を組み合わせて編集できるようにするため。画像やSVGの入力に進む前に、正本の節点を増減する経路を整えた。

ユーザーにとって何が良くなったか

矩形・楕円の輪郭を追加し、既存の面への変更を使って埋め縫いを作れる。既存点の移動だけでなく、点の追加・削除で輪郭を調整できる。各操作はUndoで戻せる。

技術的な内容

既存のVectorPathと保存形式を使い、図形を編集可能な正本として保持する。サテンは左右レールの対応を維持するため両方へ同じ節点操作を適用する。面は選択外周または穴を編集し、交差や退化は既存の検証を通して拒否する。ロックを検査し、検証が失敗した操作は文書・履歴へ反映しない。

ハマったこと・学んだこと

手動針落ちは閉鎖フラグがあっても始点へ自動で戻らない。節点追加でも末尾から始点への辺を仮定しないよう、生成と同じ意味を使う。有限の寸法でも巨大座標への加算や極小寸法によって輪郭が退化するため、生成後の境界も確認する。

自動検証

Core 58件、iPadシミュレーター合同101件(Coreを含む)成功。寸法異常・節点の境界・ロック・履歴・不正形状の原子性を確認した。

複数図案の編集とPencilの一針入力を追加(未リリース)

feature開発中(未リリース)

複数図案の編集とPencilの一針入力を追加(未リリース)

何を変えたか

複数選択、グループ、レイヤー、表示・ロック、整列・中心分布・格子状繰り返しを追加した。Pencilのタップを一針として置き、最後の点を戻す・確定・破棄する専用入力を追加した。実装段階で未リリース。手動操作とミシン実機検証は最後に行う。

なぜ変えたか

単体の線の編集から、複数の部品を組み合わせた刺しゅう制作へ進めるため。

ユーザーにとって何が良くなったか

複数の図案をまとめて移動・変形・複製でき、Undo一回で戻せる。レイヤーの非表示は出力対象の切り替えになり、ロックした図案を誤って変更する操作を拒否する。

技術的な内容

Swiftコアの編集セッションでグループ展開・ロック検査・原子的な更新を処理する。非表示でも保存正本の幾何と上限は検証し、針生成から除外する。レイヤー移動によって縫い順は変えない。旧アプリが新しい表示属性を無視して出力することを避け、文書をv2へ更新した。v1は検証して移行する。

ハマったこと・学んだこと

Pencilの入力開始時の座標変換と世代IDを保持し、モード変更後の遅延通知を破棄する。未確定の点列は文書へ混ぜず、確定成功時だけ一つの編集履歴にする。失敗時には点列を保持する。グループの順序変更は選択された個別メンバーの位置ではなく、グループ先頭から計算する必要がある。

検証と今後

コアのロック迂回・履歴・保存移行・生成除外と、アプリの複数選択・Pencil通知・下書き復元を自動テストした。iPadシミュレーター合同91件成功(Core 51件を含む)。実機のPencil操作と縫製品質は未検証。画像・SVG・文字・写真変換や多様な刺しゅう方式は後続項目で、競合製品の全機能と同等になったことを意味しない。

サテンと穴付き面を追加し、編集正本と生成区間を保持する

feature開発中(未リリース)

サテンと穴付き面を追加し、編集正本と生成区間を保持する

何を変えたか

ランニングのみだった初期実装へ、ビーン、手動点列、サテン、穴付きのタタミを追加した。糸色・複製・縫い順・移動・回転・拡縮・反転、左右レールと穴の編集をiPadのインスペクタへ接続した。設定変更はUndo/Redoと保存・再読込に反映する。

開発中・未リリース。 全競合機能の同等性を達成したという意味ではない。手動操作とミシンでの確認はユーザーの方針に従って最終工程にまとめ、現時点では未実施。

なぜ変えたか

刺しゅう制作の高度な編集へ進むため。生成済みの針だけを保存すると、サテンの幅・密度や面の穴を後から変更しにくい。左右レール、外周、穴、生成設定を編集可能な正本に残した。

ユーザーにとって何が良くなったか

線から種類を変更し、サテンの間隔・引き補正・センター下縫い、面の角度・行間隔を調整できる。Pencilで穴の輪郭を追加できる。図案全体の変形は右レールや穴にも適用し、針経路を設定した糸色で表示する。

技術的な内容

面を走査して得た連続区間を別々のrunとして持ち、間をジャンプ命令にする。穴をまたぐ部分を通常の縫い線にしないため、面を平坦な点列へ変換するAPIは拒否する。サテンは対応するレール間を補間し、長い針目を分割する。

形状の自己交差や穴の包含を検査し、辺同士の検査量・走査量・頂点数・針数を制限する。計数時は針出力配列を生成しない。編集セッション内では検証済みオブジェクトの完全一致を確認して結果を再利用し、変更された形状は再検証する。

ハマったこと・学んだこと

  • 角度付き面の交点を逆回転すると、枠境界の0がごく小さい負数になりDST出力で拒否された。交点を元の辺上で補間して境界を保持する方法へ変更した。
  • 手動の針落ち点は補間しないため、DSTの一針の上限を超える区間が存在しうる。出力と同じ量子化後の各軸差を検査し、書出し前に表示する。
  • レールや穴は別々に変形せず、全正本へ同じ変換を適用する。種類変更で不要になった設定も、Undo時は復元する。
  • 大きな図案で全オブジェクトの幾何検証を編集ごとに繰り返すと操作を待たせる。完全一致の検証キャッシュを使い、無効入力や全体上限のチェックを維持しながら変更分だけ再計算する。

検証した事実

独立readerのpyembroidery 1.5.1で、ビーン7針・手動3針、下縫い付きサテン28針・穴付き面60針を照合した。往復順・下縫い・色替え・穴の空白・ジャンプの一致を確認した。モデル・履歴・保存・無効幾何・上限・非同期世代について、iPad Simulatorの合同自動テスト68件(コア42件を含む)が成功した。

Macの最適化ビルドでは、2,000頂点の面50個を含む図案で、変更1個の色を検証する時間は約6.8ms、順序変更は約0.027msだった。iPad実機の性能保証ではない。

iPadの初期スライスを実装:編集正本とDST出力の分離

feature開発中(未リリース)

iPadの初期スライスを実装:編集正本とDST出力の分離

何を変えたか

仕様文書だけの状態から、SwiftUIアプリと純Swiftコアを実装した。Pencilでライン入力し、点やステッチ長を変更、Undo/Redo、針経路の確認、.stitch保存と再読込、復旧用下書き、DST書出しをつないだ。

開発中・未リリース。 iPad実機のPencil操作、Filesからの手動操作、独立ビューアの目視、性能測定、ミシン試縫いは未実施。MVP全体の完成や機種互換性は保証していない。

なぜ変えたか

手描き入力から機械出力までの一連の流れを、編集可能なデータのまま検証するため。仕様レビューで不足していた再編集、保存導線、線間の移動、座標契約も実装計画へ反映した。

ユーザーにとって何が良くなったか

初期実装では、描いた線を針データに固定せず、後から形状や針目の長さを変えられる。意図しない線間の接続縫いをジャンプに分け、問題のあるオブジェクトを選択して確認できる。一般利用者への提供はまだ行っていない。

技術的な内容

PencilKitのインクは確定後にmm単位の折れ線へ変換してクリアし、正本から表示する。純Swiftコアにモデル、上限検証、ランニング生成、針順序、履歴、保存codec、DSTを分離した。生成はバックグラウンド実行し、リクエストが更新された場合は旧結果を採用しない。

DSTはフープ中心基準へ座標変換し、絶対座標を0.1mm単位へ丸めてから差分を計算する。長いジャンプは分割する。編集正本は別のバージョン付きパッケージに保存する。

ハマったこと・学んだこと

  • 入力時にフープ内でも、出力量子化後に枠外になる場合がある。丸め前後の両方で範囲チェックが必要だった。
  • きわめて短い辺では浮動小数点の計算結果によって分割数が0になりうる。最小1分割を保証し、上限を配列確保前に検査した。
  • Pencilの完了通知は遅れて届く場合がある。Undoや文書変更後の旧入力を世代IDで拒否する回帰テストを追加した。
  • Filesで文書を複製すると文書内IDも複製される。復旧先を文書IDだけで決めず、保存URLも組み合わせて分離した。
  • 下書き1件の破損で一覧全体を失敗させず、破損ファイルを表示しながら正常な下書きを復元できるようにした。

検証した事実

SwiftコアとiPad Simulatorでモデル・生成・履歴・ファイル往復・非同期競合・Pencil人工ストロークをテストした。別ライブラリpyembroidery 1.5.1で独自DSTを読み、22針、8ジャンプ、1色替え、70×55mmの寸法、全針座標とヘッダを照合した。独立readerでの検証は、独立ビューアの目視や実機試縫いとは区別する。

TripSupporter

複数人の旅行で、位置共有・しおり・割り勘などをまとめて扱うための旅行サポートアプリです。

公式ページ

Trip SupporterのE2Eを機能別に実行し、独立した後続検査を続ける

test未リリース

Trip SupporterのE2Eを機能別に実行し、独立した後続検査を続ける

リリース状況

未リリース。開発用E2E基盤の変更であり、アプリ本番の配信は行っていない。 ローカルmain統合時点での実装・検証記録として残す。

何を変えたか

機能とケースを選んで実行できる入口、明示した依存関係、機能別の検証履歴を追加した。 通常のケース失敗では、依存する検査だけをスキップし、独立したケースを続行する。 環境クラッシュや復旧不能では安全に停止し、未着手を区別する。

なぜ変えたか

長い一括E2Eが途中で止まると、後続の問題が未検出のまま残り、修正と再実行を繰り返すため。

ユーザーにとって何が良くなったか

開発者が修正した機能だけを反復確認し、独立した不具合を一度の実行で収集できる。 アプリ利用者向けの画面変更はない。

技術的な内容

Nodeの共有実行基盤で、ケースの入口となるデータ・画面準備とjob間の依存を宣言する。 依存失敗はblocked、環境停止などによる未着手はnot-runに分け、根本の失敗jobを保持する。 台帳にはソースコミット、実行範囲、件数、機能自身の開始終了時刻・所要時間、終了処理の成否を保存する。 部分成功、古いコミットの成功、未コミット変更がある状態を機能全体の現在の合格にしない。

Before / After

Before

通常失敗の後に独立した検査も未実行となり、長い全体再実行へ戻ることがあった。

After

機能・ケースを指定でき、通常失敗の依存先だけを止める。全件実行は明示的な指定を必要とする。

判断したこと・トレードオフ

固定の分割数より、機能の境界と独立した準備を重視した。 書き込みを複数ユーザーが読む場合、reader同士を直列依存にせず、それぞれwriterへ依存させる。 共通環境の準備時間は残るため、分割だけで一定時間内の完了を保証しない。

ハマったこと・学んだこと

  • 直前jobへの一律依存では独立readerまで止める。画面遷移とデータ依存を分けて点検する必要がある。
  • 失敗後のデータ全体リセットは成功済みの分岐を壊し得る。独立ケースの入口を明示する。
  • キャッシュ分離のテストは切替前のデータを実際に読み込まないと意味が薄れる。
  • 終了処理の失敗を結果確定後に知ると誤った合格が残る。所有資源の終了後に合否を確定する。
  • wall clockの日時とmonotonic clockの所要時間を別に記録する。

Trip SupporterのUI E2E失敗を、アプリ・通信・テストドライバーに分けて調査した

bugfix未リリース

Trip SupporterのUI E2E失敗を、アプリ・通信・テストドライバーに分けて調査した

リリース状況

アプリは未リリース。2026-10-15に修正後のチャット24件の限定UIテストが全成功し、後片付けまで完了した。独立レビューとCI成功後、PR #45をmainへマージした。本番アプリの配信・TestFlight提出は行っていない。

何を変えたか

一般メンバーが権限の状態を確認でき、変更操作はリーダーだけができるように表示を統一した。画像削除時のエミュレーター接続先の判定と、iOSの画面再利用時に残るアクセシビリティ状態も修正した。

同時に、共有E2E環境の通信・初期データ生成・画面操作を修正した。画面の確認だけでなく、Firestoreの値、Storageの画像バイト列、再起動後の保持、変更しない操作の不変性を検証する方針は維持した。

2000文字の写真コメントを送信した後、値は空でも入力欄が巨大な高さのまま残り、メッセージ一覧を覆うアプリ側の不具合も確認した。長文時の高さ上限と空欄時の最小高さへの復帰を、測定用の追加stateや再マウントに頼らず修正した。

なぜ変えたか

共有UIテストの実行が96成功・17失敗・356未実行で終了した。個々の失敗には、アプリの不具合、テストの操作対象のずれ、エミュレーターの通信、OSの写真ピッカーとテストドライバーの境界が混在していた。

ユーザーにとって何が良くなったか

権限の状態が見える一方で、許可されない操作を始めてから失敗する混乱を減らす。画像削除や日付選択などの修正を、実データまで含めたテストで確認できるようにする。

長文コメント送信後もメッセージ一覧へ戻れ、送った写真を確認できるようにする。

技術的な内容

  • Firestore Emulator 1.21.0への有効なListen接続で、Apple SDKのHTTP/2 PINGを契機にtoo_many_pingsで切断する現象を再現した。E2E専用プロキシでPINGを終端し、データとエラーの意味は保持する。本番の接続経路は変更しない。
  • Storage読み取りで、Keep-Aliveソケットの再利用とサーバーの切断が重なる競合を別の再現実験で確認した。E2EのAdmin Storage接続でソケットを再利用しない設定にした。元の失敗と同じソケットの記録はないため、元ログへの因果関係は推定を含む。
  • React Native Fabricの再利用時に古いアクセシビリティ属性が残る問題を、同じバイナリーの修正有効・無効で比較した。すべてをfalseにするのではなく、保持しているpropsが要求する値へ戻す。
  • プロフィールの公開用データは、実際のFunctions変換関数から生成する。非同期の公開反映が完了する前に「変更なし」の基準を作らない。比較を緩めるのではなく、初期化の責任と完了条件を正す。
  • 写真ピッカーの見た目が正常でも、ホストアプリ経由のXCTestスナップショットは空になる場合があった。同じ停止状態で写真ピッカー自身を取得すると、Cancelと実際の写真グリッドを読めた。ホストとリモートのAX結合境界として切り分けた。

Before / After

Before

ラベル変更や固定ヘッダーとの重なりにより、テストのタップ先が意図とずれていた。OSの写真ピッカーが表示されているのに、ドライバーが取得するツリーにはステータスバーだけが残ることもあった。

After

通常のスクロールで操作対象を露出させ、遷移後の表示と保存結果を確認する。写真ピッカーについては、表示中の実ウィンドウと操作可能なCancelを検証して、現在のピッカーのスナップショットを取得するドライバー拡張を実装した。

判断したこと・トレードオフ

  • UIテスト全件は再実行せず、元の失敗と修正の影響範囲を限定して実行する。通常の型・構文・lint・全Jest/Node検査は実施する。
  • 写真ピッカーを閉じてもプロセス状態はforegroundのまま残り得る。プロセス状態だけでは表示中と判定せず、現在のウィンドウの存在と操作可能性を使う。
  • アプリ再起動、固定座標による画像選択、無条件リトライ、過去スナップショットの流用で失敗を隠さない。
  • 元のMaestroインストールは変更せず、固定版ソースとハッシュを検証した専用コピーを使う。

ハマったこと・学んだこと

「UIが見えない」という同じ症状でも、Fabricの再利用属性とPHPickerのリモートAX取得は別の原因だった。最初の修正を別の失敗にも当てはめず、同じ停止状態でホストとピッカーを直接比較すると調査の境界を絞れた。

画面が見えていること、アクセシビリティ要素が存在すること、タップが目的のコントロールへ届くこと、バックエンドが保存されたことは、それぞれ別の証拠として扱う必要がある。

2000文字の入力確認では、XCTestのsnapshot値が512バイトで省略される境界にも遭遇した。Appium公式の説明に同じ制約がある。実アプリの停止状態を保ち、唯一の入力欄に2000文字が完全に入っていることと、元snapshotの512文字を読み取りだけで比較した。別の小さいUIKitアプリでは、native値とXCTestの直接値が2000文字、snapshotが512文字になることも確認した。実アプリの同じテストランナーは終了済みで、そちらの直接値を追加取得した証拠はない。

長文の検証は、取得できないsnapshot全文一致に依存せず、UIの入力・添付・実メッセージ表示と、DBに保存された2000文字全文の完全比較へ分担する。512・1999・2001文字の誤保存を拒否する検査は残す。サービスは2000文字超を拒否し、勝手に切り詰めないため、2001文字のUI入力から2000文字が保存されることは入力上限の検証にもなる。

集金編集では、期待データを作るテスト自身が誤りを再現していた。正規UIは参加者IDを辞書順にして保存するが、テストは初期fixtureの順を期待していた。また、正規モデルとwriterに存在しない更新者フィールドを期待し、単体テストのAdmin書き込みもその架空フィールドを付けていた。実装・モデル・ルールを照合して期待を修正し、余分な項目、重複、欠損、別ユーザー、非正規順序の拒否を維持する。

Firestoreの保存フィールドに入るserverTimestampと文書metadataのupdateTimeも同値ではない。REQUEST_TIMEは処理時刻をミリ秒精度で表し、WriteResultのupdateTimeは適用後の文書更新時刻を表す。実測でも差があった。値を丸めて一致させず、既存の共通検証で形状・精度・更新前の読み取り時刻から更新後のmetadata時刻までの範囲と、フィールド・metadata両方の前進を検査する。他の保存項目は引き続き完全比較する。

2枚画像の最後の検査では、元の素材と本体writerが正式対応するHEICを、検証側の3か所がPNG/JPEGに限定していた。全形式を曖昧に許可するのではなく、チャットだけにHEIC検証を明示し、拡張子・contentType・画像の識別情報・サイズ・保存バイト列のdigestとgrantの対応を保つ検査へ修正した。共通のPNG/JPEG検証は変更しない。

診断保存にもライフサイクルの検証が必要だった。小さいネイティブプローブでは取得できたランナー内のファイルが、実際のMaestro CLI終了後にはランナーのアンインストールで消えていた。ログインUIが成功していても診断回収が失敗した実行は停止し、業務ケースの成功に数えなかった。公式の--no-reinstall-driverを専用Simulatorに限定して使い、CLI終了後もランナー内の原情報を回収する。正常終了と意図的な失敗終了の両方を実CLIで測定し、同じホストアプリのプロセスで回収できること、最後に専用Simulatorを削除できることまで確認した。

入力の消去でも、数値と長文では同じメニューにならなかった。長押しで文字列が選択されると、「Select All」ではなく「Cut」が表示された。ただし選択成立は全文選択を意味せず、削除後の残存文字の検査が部分選択を検出した。Maestroの独立したHTTPタップ3回も、OSの段落選択を保証しなかった。実アプリのnative選択範囲は2001文字中の末尾157文字で、UIKit単体の全文選択とは異なった。操作候補の採用前に実アプリで選択範囲と削除後の空欄を確認する必要がある。

その後、実アプリで本文2001文字・末尾caret・キーボード表示・focusを確認して通常削除を送ったところ、全コマンドが完了しても本文は1文字も減らなかった。React Native 0.86.3のFabric入力実装は、既存文字数がmaxLengthを超えた状態で1文字を削除すると、許容長の計算が0になって編集を拒否する。写真添付前の2001文字に、添付後の2000文字制限を掛けた状態が該当した。写真添付時のnative上限を通常の2000文字と既存draft長の大きい方に修正した。既存の過長draftはそれ以上増やせず、削除で短くすると上限も縮み、2000文字以下で通常の上限へ戻る。修正後は実アプリで2001文字の通常削除と厳密なUI空確認が通過した。削除後0文字のnative直接測定は行っておらず、この結果はUI証拠として扱う。送信サービスの過長拒否は維持する。

ドライバーの追加検証自体が原因になる場合もあった。写真ピッカーのsnapshot取得後にCancel.isHittableを問い合わせると、hit pointを計算できずXCTestサーバーそのものが失敗して終了した。取得済みの同一snapshot内でボタンの識別子・種類・一意性・enabled・有効なframeを検査する設計へ見直した。実際に押せることは元のUI操作で確認し、検証用の追加問い合わせで生じた例外をretryで覆わない。

割り勘から集金し、チャットのマネタローで進捗を確認する

feature未リリース

割り勘から集金し、チャットのマネタローで進捗を確認する

リリース状況

未リリース。実装・ローカル検証・独立レビュー・CIを完了し、PR #40をmainにマージした。本番のFunctions・Rules・索引の反映、アプリ配布、OSプッシュの実配信は未確認。

何を変えたか

既存の割り勘画面に集金を追加した。精算結果から作る方式と、受取人・対象者・金額を入力する個別請求方式を選べる。

支払者が報告し、受取人が確認すると明細が完了する。チャットには固定アイコンのマネタローが開始カードを投稿し、現在の進捗から詳細を開ける。全員の受領確認が終わると完了カードを一度投稿する。

未払い者への手動催促と最大3件の予約を用意した。報告済みの人を除き、同一集金・同一対象者への催促は1時間空ける。送金自体は行わない。

なぜ変えたか

割り勘の計算結果を出した後も、支払いと受領を確認する作業が残る。旅行のチャットから集金の状態を確認できるようにし、個別の連絡と照合の手間を減らすため。

ユーザーにとって何が良くなったか

計算結果を転記せず集金を作れる。独立した請求も選べ、支払い報告と実際の受領確認を区別できる。催促の対象を確認してから送信し、予約もできる。

技術的な内容

React Native / Expo と Firebase を使用。金額は通貨の最小単位の整数で扱い、Functionsとアプリが同じ純粋モデル・parser・精算計算を使う。Callableのトランザクションで明細、履歴、bot投稿、通知を更新する。

精算方式ではサーバーが結果を再計算し、集金後の支出・通貨・為替・連動する旅程費用をRulesでも固定する。個別請求だけなら支出を固定しない。完了や予約の二重実行を防ぎ、通知配送は送信直前にも設定と支払い状態を確認する。

Before / After

Before

割り勘の精算結果を見て、集金と受領の管理は別に行っていた。

After

割り勘から作成方式を選び、チャットのbotカードと明細で支払い報告・受領確認・催促を管理できる。

判断したこと・トレードオフ

  • 報告だけでは完了させず、受取人の確認を必要とした。
  • 共有チャットは開始と全員完了を中心にし、個々の報告で投稿を増やさない。
  • 精算に影響しない旅程は編集・削除できるよう、固定対象を費用と連動支出に限定した。
  • 実プッシュ配信とエミュレーター上の通知レコード検証を分けた。

ハマったこと・学んだこと

  • 作成Callableが成功しても購読データはまだ届いていない場合がある。作成済みIDの到着待ちを、既存IDの欠損エラーと区別する必要がある。
  • 請求方式を先に分岐しないと、受取人未選択の個別フォームに別方式の精算明細を表示してしまう。未選択は明示的な空状態として扱う。
  • 開発サーバーのlocalhostがIPv6だけにbindすると、IPv4へ接続する開発アプリが起動できない。専用環境でIPv4接続を確認した。
  • ローカルの設定ファイルと、端末にインストール済みのアプリのFirebase projectは同じとは限らない。実アプリのplistを確認し、Emulatorの起動・seed・Callableを揃えた。
  • EmulatorのStorage RulesからFirestoreを参照するテストは、起動projectとテストnamespaceを揃える必要がある。

Trip Supporterの招待機能を最新通知・退会処理へ統合(未リリース)

feature未リリース

Trip Supporterの招待機能を最新通知・退会処理へ統合(未リリース)

何を変えたか

旅行招待・参加承認の開発PRへ、最新mainの通知の検証と退会処理の改善を取り込んだ。2026-10-06の実装履歴はそのまま残し、今回は競合解消と統合後の検証を記録する。

なぜ変えたか

招待機能の開発中に、既存機能の通知生成と退会処理が更新された。単に競合箇所を選ぶだけでは通知の重複防止や退会の再試行が失われるため、両方の保証を維持する必要があった。

ユーザーにとって何が良くなったか

招待申請の通知が再送されても、同じ通知の既読状態や送信済み状態を保つ。申請を審査した管理者が退会するときも申請者の情報は維持し、審査者の識別情報を匿名化できる。現在は未リリースで、利用者へ配布していない。

技術的な内容

  • 通知イベントはサーバーからだけ作成できる権限ルールを維持した。既存の通知リクエストAPIでは許可された操作だけを検証し、招待イベントを利用した任意通知生成は拒否する。
  • 宛先ごとの固定通知IDとcreateによる重複防止を招待通知にも適用する。並行再送でも受信箱と送信キューを各1件に保ち、既読やsentを上書きしない。
  • 退会処理は対象を検索して処理記録を保存し、transaction内で最新データを取得して匿名化する。最後に保存した対象を独立して検査する。
  • 申請書類のreviewedByも退会の検索条件に追加した。collection-group indexは検索フィールド集合から生成し、joinRequestsも同じ集合に合わせた。
  • Emulatorのテスト接続先は必須の環境設定から読む。mainに追加された固定ポートのテストも、この明示設定へ統一した。

判断したこと・トレードオフ

  • 最新mainの通知検証・退会対象の限定・transaction保証を保ち、招待の宛先や申請IDを追加した。
  • 匿名化後は検索に使うUIDがなくなるため、処理前に保存した対象パスを再試行でも確認する。
  • 自動検証の成功と端末での導線確認は別に記録する。当初の実機確認方針から、後述するユーザー指定のシミュレーター確認へ変更した。

ハマったこと・学んだこと

  • reviewedByを匿名化するコードだけでは不十分だった。対象発見クエリとindexに同じフィールドがなければ、審査者UIDだけ残る申請を見つけられない。
  • 退会処理がtransaction方式へ変わったとき、古いbatchのmockは実際の保証を検証できなくなる。最新readと処理記録の保存を確認するテストへ更新した。
  • 匿名化のcommit直後に失敗を注入すると、UID検索から消えた対象でも再試行できるかを確認できる。正常系の退会テストだけでは、この境界を検証できない。
  • 通知は受信箱と送信キューの重複件数に加え、再送後の既読・sent保持まで確認する必要がある。

2026-10-07 ローカル画面確認での追加修正(未リリース)

ユーザーの指定で録画とユーザー所有の物理iPhoneの使用を停止し、既存iPhone 17 Proシミュレーター(iOS 26.5)へ切り替えた。他アプリや既存コンテナは削除せず、専用bundleで検証する。

署名を不要とするシミュレーター用native Debugビルドが成功した。MetroのlocalhostがIPv6だけをlistenすると、native側が生成する127.0.0.1のbundle URLから接続できなかった。NodeのDNS順序をIPv4優先に指定し、loopback内で同じMetroへ接続した。

画面レビューでは、招待URLをExpo Routerが通常ページとして扱いUnmatched Routeになる不具合を発見した。+native-intent で既知の招待を共通認証入口へ送り、ログイン前から招待intentを保持する。専用dev-client起動URLや通常画面パスは変更しない。TDDで5件の失敗を確認した後に修正し、最新head 3ec6a1e433a2751fac357c8e0f310c77424875b9 の全verifyは258スイート・1680テスト成功。Rulesは変更していない。

新headのCIと独立シミュレーター再確認は進行中。シミュレーター確認を実機確認済みとは扱わず、本番配信も行っていない。正式記録: https://github.com/sponge841841/tripsupporter/pull/39#issuecomment-6036602862

2026-10-07 追加検証と再開条件(未マージ・未リリース)

head 3ec6a1e の必須CI verifyは成功した。独立コード再レビューも追加必須指摘なしで、native-intentと通知ルーティングの関連2スイート・21テストを独立実行して成功。17 Proシミュレーター上でも、認証前の招待URLでUnmatched Routeが再発せずログイン画面を保持することを確認した。

ログインは一般失敗表示になった。同じ合成fixtureによるAuth Emulator RESTログインは200・UID一致のため、native側の切り分けを継続している。Keychainアクセス不足は仮説であり、原因を断定していない。Apple資格情報不要のSimulator ad-hoc署名で専用アプリを再ビルド・インストールし、両方成功した。demo Firebase設定、専用bundle、原本設定の復元とGit cleanを確認済みだが、認証の改善は画面未確認。

現在の外部依存はMacのロック解除操作。ユーザー所有の物理iPhoneと録画は使用しない。同じworktree・head・既存17 Proシミュレーターで、解除後に専用アプリを起動し、認証後の招待保持、即時参加、申請・承認・拒否・再申請、共有キャンセルを再開する。これらの受入確認と最終判定が済むまでdraftを維持し、マージしない。未リリースであり、本番データ移行・配信・ストア提出も未実施。

必須CI: https://github.com/sponge841841/tripsupporter/actions/runs/37611670193/job/112760113853

2026-10-07 最終独立受入・マージ完了(未リリース)

Mac復旧後、既存iPhone 17 ProシミュレーターとローカルEmulatorで独立確認を再開した。認証前の招待はログイン後に保持され、即時参加、承認前の非加入、拒否・再申請・承認、承認後の対象旅行入場、壊れた招待の安全なエラーを確認した。管理者の旅行ID・パスワード表示と共有シートを確認し、送信せず閉じても失敗表示が出なかった。作成フォームの承認要/即時の選択も切り替え確認済み(フォーム送信はしていない)。開発buildのcold起動はdevLauncherでMetroを手動選択したため、配布buildのcold起動と同一とは扱わない。

途中のUI確認で、一般メンバーにも招待ボタンが表示され、管理者専用APIが拒否する不整合を発見した。詳細権限と一覧表示をactive leader専用に合わせ、取得済みメンバー情報と明示userIdから許可を判断する。未取得状態を推測で補完しない。一般メンバーの非表示操作とアバター表示は維持した。TDDの失敗確認後、ローカルコミット d69fc3429cd83af2b3e96abb5491c750bc3407f5 に固定し、独立コード再判定・関連3スイート31テスト・full Reload後のmember制限/leader共有UI確認を完了してからまとめてpushした。

最終全verifyは259スイート・1686テスト成功。Rulesは前段の15スイート・116テスト成功を保持し、最終headの必須CIでも権限テストとrepository verifyが成功した。独立最終判定は必須指摘なし・選択されたローカル受入成功。通常マージでPR #39がmainへ入り、GitHubのMERGED状態とmainのmerge commit 9cb883c2219cd3b45c1aaac4f72d6599472fa5ed を確認した。

マージ済みだが未リリース。 本番データ移行・Functions/Rules/Hosting配信・ストア提出は行っていない。実push、正規HTTPS Universal Links、LINE実送信、未導入実App Store画面は配布前の未検証範囲。ユーザー所有の物理iPhoneと録画は使用せず、既存アプリやデータも削除していない。リリース済みのお知らせは公開していない。

Trip Supporterの旅行招待と参加承認を実装(未リリース)

feature未リリース

Trip Supporterの旅行招待と参加承認を実装(未リリース)

何を変えたか

OSの共有シートで旅行の招待URL、参加旅行ID、パスワードを共有する機能を実装した。旅行作成時に即時参加と管理者承認を選べる。承認制では参加申請、承認・拒否、結果確認、再申請を扱う。

なぜ変えたか

LINEなどで旅行への招待を送り、受け取った人が参加情報を再入力せず参加できるようにするため。管理者が参加者を確認してから加入させる用途にも対応する。

ユーザーにとって何が良くなったか

招待リンクから参加画面へ進める設計になった。ログイン前に受け取った招待を保持する。未導入の場合はApp Storeへ誘導するページを用意した。実機での導線確認は未完了であり、配布済みの機能ではない。

技術的な内容

  • Firebase Functionsが招待発行・解決・参加・承認を処理する。申請者がFirestoreに直接メンバー加入を書き込めないルールにした。
  • 承認はtransaction内で現在の管理者、アカウント状態、申請IDを確認する。過去の申請を承認して最新の再申請へ適用することを防ぐ。
  • 招待の有効性は旅行の現在の認証情報に結び付け、パスワード変更後の古い招待を拒否する。
  • 招待のHosting、AASA、native設定は共通設定を使う。dev検証では別bundle・schemeを使い、本番アプリの認証状態を保持する。
  • 既存旅行の参加方式フィールド欠損を明示移行するdry-run/applyスクリプトを用意した。更新時刻の事前条件と途中失敗レポートを記録する。

Before / After

Before

旅行の参加情報を渡し、受け取った人が入力して参加する方式だった。

After

URL共有と、旅行ごとの参加承認を実装した。現在は開発ブランチ上の変更で、利用者には配布していない。

判断したこと・トレードオフ

  • 承認待ちの人は旅行メンバーに追加しない。本人の申請だけ参照できるようにする。
  • 不整合や設定欠損を既定値で補わず、明示エラーにする。
  • 新APIは参加方式の指定を要求する。旧アプリからの旅行作成が失敗するため、旧版の更新導線を保証するまで本番Functionsを切り替えない。
  • Webでは導入済み判定を確実とは扱えない。アプリを開いた後にStoreへ遷移しない制御と、実機のSafari・アプリ内ブラウザ確認が必要になる。

ハマったこと・学んだこと

  • 通知payloadに旅行IDを入れるだけでは、通知タップから該当画面へ移動できない。アプリ終了時とバックグラウンド時の通知入口を接続する必要がある。
  • エミュレーターのportをテスト側に固定すると、並行作業のport競合を避けられない。起動ツールが渡す必須endpointを検証して使うようにした。
  • 実機のUIテストにはアプリだけでなくXCTest runnerのdevelopment署名も必要だった。開発アカウント未設定のため、実機確認は停止中。

スケジュール画像の削除と、縦横どちらの写真も切り抜かない表示

feature未リリース

スケジュール画像の削除と、縦横どちらの写真も切り抜かない表示

何を変えたか

Trip Supporter のスケジュール編集画面に「画像を削除」を追加した。共通の確認モーダルで確認した後、画像をフォームから取り除き、スケジュール保存時に変更を確定する。確認をキャンセルした場合、登録済み画像は変更しない。

編集プレビュー、詳細画面、一覧サムネイルの写真を切り抜かず、全体を表示するようにした。縦向き・横向き・正方形の写真に同じ表示方式を使う。写真選択時の編集・切り抜きは引き続き無効にしている。

実装は手元のチェックアウトに反映し、ストア配布は未実施。未リリースのため、アプリ内お知らせは掲載していない。

なぜ変えたか

ユーザーから、一度スケジュールに登録した写真を削除したいという要望があった。さらに、縦向き・横向きの写真を表示枠に合わせて切り抜かず、写真全体を表示したいという要望があった。

ユーザーにとって何が良くなったか

画像の変更だけでなく、写真のないスケジュールへ戻す操作ができる。削除確認と保存の段階を分け、誤操作を防ぐ。写真の端にある情報も、表示時のトリミングで隠れないようにした。

技術的な内容

  • 専用の ScheduleImageField が画像のプレビュー・変更・削除確認を担当する。画像ありの場合は写真に操作ラベルを重ねず、写真の下に変更・削除操作を表示する。
  • CachedImage / expo-image の contentFit を contain にして、縦横比を保持しながら全体を枠内へ収める。memory-disk キャッシュを維持する。
  • 編集フォームで画像なしは空文字、保存入力では null として表現する。create/update の入力型に明示的な画像なしを表す string | null を定義する。
  • 更新時に imageUri が未指定なら既存画像を維持し、明示的な null の場合だけ画像参照を解除する。DB の merge 更新で古い画像を残さない。
  • 一覧サムネイルは予定 ID による recyclingKey と、スクショ共有時の画像読込み完了・失敗の通知を維持する。

Before / After

Before

編集画面に画像の削除操作がなかった。写真は cover で表示され、表示枠と写真の縦横比が違う場合に端が隠れていた。

After

画像を確認後に取り除き、保存して確定できる。編集・詳細・一覧の全表示で写真全体が見える。

判断したこと・トレードオフ

表示枠のサイズは維持し、縦横比が違う場合は余白を許容した。画面レイアウトを写真の寸法取得に依存させずに、切り抜かない表示を実現した。大きな編集画面に削除確認の状態を追加せず、画像操作を専用ファイルへ分離した。

ハマったこと・学んだこと

この作業の対象チェックアウトでは、フォームの空文字を undefined に変換し、その項目が DB 更新時に除外されていた。画像なしを明示的な null として保存することが、削除を永続化するために必要だった。

ただし、2026-10-03 の写真永続化修正(PR #28、2026-10-04 に GitHub main へマージ)でも画像なしの null 保存は対応済みである。今回の手元のチェックアウトにはその修正が未反映だったため、GitHub の最新 main で再発した不具合とは扱わない。今回のコードを統合する際は、既存の Storage 保存処理を維持したうえで削除 UI と全体表示を反映する必要がある。

スケジュール写真の改善と、レビューで発見した暗黙補完の是正

fix未リリース

スケジュール写真の改善と、レビューで発見した暗黙補完の是正

何を変えたか

登録済み写真の削除と、縦横どちらの写真も全体を表示する変更を、写真Storage永続化対応を含む最新mainに統合した。共通確認モーダルを経由し、保存で画像参照の削除を確定する。編集を保存せず閉じる場合や確認取消では登録済みの写真を保持する。

PR作成後、実装担当とレビュー・動作確認担当のサブエージェントが直接指摘と修正をやり取りした。レビューで見つけた既存フォールバックについてもユーザーの追加依頼を受け、正常な未設定と不正なデータを分けるよう修正した。

なぜ変えたか

写真の操作を改善するだけでなく、取得失敗や不正な値を別の値に置き換えると、ユーザーが意図しない設定や支払対象を保存する可能性があるため。

ユーザーにとって何が良くなったか

  • 写真を削除し、再選択できる。写真の端が表示時に切り抜かれない。
  • 不明なカテゴリを先頭カテゴリへ変更したり、支払者を別の人へ選び直したりしない。非同期のメンバー取得で解除済み参加者を全員へ戻さない。
  • 不正な日付・時刻・金額・座標・設定は入口で拒否する。不正金額を『費用なし』として扱い、既存の割り勘を削除する経路を防ぐ。
  • 地点全削除を表す空配列を読込みでも保持し、古い地点を復元しない。
  • タイムゾーンや通知の欠落を勝手に東京・通知オン・5分前へ変えない。必要な場合は設定を求め、時刻未定や通知offでは不要な設定を強制しない。
  • OGプレビュー取得失敗と電話起動失敗を明示し、成功扱いや別方式への暗黙再試行を行わない。

技術的に学んだこと

保存だけでなく、読取mapperと表示モデルの変換まで確認する必要がある。明示的なlocationPins=[]をundefinedへ変えると、『未指定の旧単地点を表示する』正常な変換で削除済み地点が復元される。空配列と未指定は異なる意味を保持しなければならない。

新規作成時の初期値と、保存済みデータの欠落補完は別の責務である。初期値はフォームで明示し、repositoryでは任意未設定をnull等で表す。不正な非空値は境界で拒否する。

終了日・終了timezone未指定を開始側と同じとする仕様、作成者と更新者を同一にする記録、ロック未設定=false、参加者未選択=[]、実住所や実座標の表示は正常な状態/既知情報の変換として維持した。SDK内部情報を安全な文言へ変換するgetErrorMessageは、失敗を正しく伝えるエラー正規化として維持した。

通知フォーム、参照データ取得、ピッカー日付、外部アクション、アイコンカタログ、入力検証を分離した。個別のアイコンなど写真以外の保存済み設定を保持する回帰も追加した。

検証と状態

  • 最終HEAD: 9dcc315771dd4afd48fc4e30416b495d9f256b19。

  • npm run verify成功: 244スイート・1,533テスト、型安全違反0件、ESLint、アプリ/Functions型チェック、Functions成果物38ファイルの同期を確認。

  • レビュー担当が最終HEADで関連14スイート・144テストを独立実行し、マージ阻害指摘なしと承認。

  • 実機/シミュレーターでの描画確認・ストア配布は未実施。未リリースのためアプリ内お知らせには掲載しない。

  • PR #34は2026-10-05 07:02:48 UTCにマージ済み。マージコミット: b115967434c2f24658b9f471d2a75bbe576f3eb1。

  • 最終HEADのCI成功。Firebase Security Rulesとrepository verificationを含む: https://github.com/sponge841841/tripsupporter/actions/runs/37275188796

  • 先の写真改善メモに記した最新mainとの統合作業は、このPRで完了。配布は今回の依頼対象外。

Trip Supporter のレビュー導線を修正

fix未リリース

Trip Supporter のレビュー導線を修正

何を変えたか

iOS の手動レビューを、公開済みアプリの App Store ID 6787812825 と action=write-review を使う導線へ変更。初回旅行完了後の自動依頼は、外部ストア遷移からネイティブレビュー API へ変更した。

なぜ変えたか

既存処理は任意の環境変数で設定したストア URL だけを参照しており、未設定の場合に何も開かなかった。自動依頼もネイティブレビュー API を呼んでいなかった。

ユーザーにとって何が良くなったか

「アプリをレビューする」から対象アプリのレビュー投稿に進めるようにする。自動依頼は旅行体験を終えた後に行い、操作中の割り込みを避ける。

技術的な内容

手動操作は Linking.openURL、自動依頼は expo-store-review の isAvailableAsync と requestReview を使用する。端末内の既存依頼履歴を維持し、同時呼び出しを抑止する。不正な履歴を未依頼として扱わない。

ハマったこと・学んだこと

レビュー要求の完了は、ダイアログ表示や投稿完了の確認にはならない。OS が表示を判断するため、ボタンからの手動操作は投稿 URL を開く方式に分ける。

Jest が通っても Expo の動的設定評価が通るとは限らない。共通定数を TypeScript ファイルから相対 import した最初の案は expo config で解決できず、独立レビューで検出した。設定評価そのものを確認する必要がある。

リリース状態

この修正は未リリース。PR のマージは App Store 配布完了を意味しない。アプリ内お知らせ機能は存在するが、今回の修正のお知らせはリリース後に掲載する。

スクショ共有で消失した予定写真が読み込めない原因を修正

bugfix未リリース

スクショ共有で消失した予定写真が読み込めない原因を修正

何を変えたか

Trip Supporterの予定写真をCloud Storageへ保存してから、HTTPS URLを予定のDBに登録するようにした。新規作成と更新の共通境界で処理する。端末内の一時ファイルを旅行の共有データとして保存しない。

写真がすでに消失した旧データには、予定の編集画面で写真を選び直すよう案内する。写真未選択を明示的なnullとして保存し、更新時に以前のURLを残さないようにした。

アプリ修正は未リリース。PR #28で実装を提出した。写真保存用Storageルールは承認を得て本番反映済み。ストア配布とは区別する。

なぜ変えたか

スクショ共有の実際の失敗をiOSシミュレーターで再現した。写真付きの予定が参照していたのは、現在は存在しないアプリのデータコンテナ内のImagePicker画像だった。端末内のfile URIがDBに残っており、CFNetworkのfile not foundエラーで写真読み込みが失敗していた。

写真のない全日程画像は生成と共有シート表示に成功したため、撮影API全体の失敗と写真読み込みの失敗を切り分けられた。フォント準備の画像生成APIも調査したが、今回の再現原因ではなかったため変更しなかった。

ユーザーにとって何が良くなったか

新しく保存した予定写真は、元の端末内ファイルを削除した後やアプリを再起動した後にも共有画像へ表示できる。写真アップロードに失敗した場合は、予定を保存したことにしない。

消失した古い写真はURLだけでは復元できず、写真の再選択が必要になる。読み込めない写真を省略して不完全な日程画像を成功扱いする処理は加えていない。

技術的な内容

  • repositoryのcreate/updateで、local URIをStorageへアップロードしてからDBを書き込む。
  • HTTPS URLはそのまま保持する。画像未指定の部分更新は保持し、明示nullは解除として扱う。
  • 存在しないローカルファイル、不正URI、0バイト画像、10MiB以上の写真を保存前に拒否する。
  • 写真保存後にDB書き込みが失敗した場合は、その試行で新しくアップロードした画像だけを削除する。削除の失敗はログに記録し、元の書き込み失敗を保持する。
  • Storage読取りはstatusがactiveの旅行メンバーに限定する。書込みはリーダー、個別の予定編集権限、旅行の全員編集設定のいずれかに従う。
  • 新しい写真はUUIDを含む別パスへ保存し、既存オブジェクトの上書きを拒否する。画像MIMEと10MiB未満をルールでも検査する。

ハマったこと・学んだこと

スクショ処理の画像読み込みエラーは、撮影処理より前のデータ保存境界に原因があった。ImagePickerのURIは選択端末上では表示できても、端末の一時領域の寿命や再インストールを越える共有URLにはならない。

Firebase Storageの新規保存ルールではresourceがnullであることも明示した。上書き拒否を単にallow update: falseへ任せず、実際のSDK uploadで検証した。

導入済みrules-unit-testingのclearStorageはルート直下のitemsだけを削除し、子prefixを再帰しなかった。テスト画像が深いパスに残るため、テスト専用エミュレーターのprefixを再帰削除して各テストを分離した。

2026-10-04の動作確認とマージ追記

上記の写真保存確認の承認待ちは解消した。ユーザーからテスト用シミュレーターの写真アクセス許可を得て、本来のアプリ画面から写真付き全日程のPNGを写真ライブラリへ保存した。成功Toastに加え、写真ライブラリの画像数が6枚から7枚へ増え、新しいPNGファイルが作成されたことを確認した。保存された画像そのものを開き、予定写真・アイコン・全4日程・末尾ブランドを確認した。

続けて共有シートへのPNG受け渡しと、シートを閉じた後の操作可能状態への復帰を確認した。個別共有先での受信は引き続き未確認。確認用の予定は通常の削除確認モーダル経由で削除した。

全件検証を再実行し、240スイート・1,451テスト成功。GitHubのVerify CIも成功。PR #28をmainへマージした。マージコミットは b01f2f03624937b0d546355d1b72ebc9b711d29e。マージ後のmainのコードツリーが検証済みブランチと一致することも確認した。

修正版アプリはストア未配布で、リリース状態は未リリースのまま。アプリ内お知らせは公開しない。

公開プロフィール取得失敗で旅行編集権限を失わないよう購読状態を分離

bugfix未リリース

公開プロフィール取得失敗で旅行編集権限を失わないよう購読状態を分離

何を変えたか

Trip Supporter の旅行メンバー購読で、権限の根拠となるメンバーデータと、表示名・アバターなどの公開プロフィールを分離して扱うようにした。

公開プロフィールの取得だけが失敗した場合は、取得済みのメンバー role や権限を保持する。一方、メンバー本体や本人参加状態の取得に失敗した場合は権限を付与しない fail closed の挙動を維持する。

プロフィール表示値の変更だけで Firestore 購読を作り直さないようにし、表示名やアバター更新のたびにリーダー権限が一時的に消える問題も防いだ。

リリース状況は 未リリース。PR #27 は main へマージ済みだが、アプリのストア配布は未実施。プロフィール同期 Functions・公開プロフィール投影・関連ルールの一部は先行して本番へ反映済み。

なぜ変えたか

従来は、公開プロフィール購読の permission error やパース失敗をメンバー一覧自体の失敗として扱い、一覧を空にしていた。その結果、Firestore上では active / leader の旅行作成者でも、アプリ上では旅行編集・開始・終了などの権限を失うことがあった。

また、プロフィール情報をメンバー state にマージした状態で保持すると、プロフィール更新を依存配列に含める必要があり、購読の再生成時に一度空配列へ戻ることで権限がちらつく問題が発生した。

技術的な内容

  • メンバー購読エラーを members と profile の発生元に分けた。
  • members エラー時は権限情報を破棄し、古い role を使わない。
  • profile エラー時はプロフィール表示情報だけを外し、取得済み role は保持する。
  • 本人の membership 購読が loading / missing / error / removed の場合は権限を付与しない。
  • Firestore購読 state には raw な TripMemberSummary[] を保持し、プロフィール編集中の表示値とのマージは useMemo で別計算する。
  • 旅行ID・認証ユーザー切替後に旧購読から遅延通知が来ても、新しい画面状態へ反映しない。
  • キック権限も、旅行作成者IDだけではなく、取得済みの leader 権限を満たす場合に限定した。

ハマったこと・学んだこと

認可に使うデータと表示用データを同じ state にまとめると、表示更新が認可状態の再計算や購読ライフサイクルへ影響しやすい。

権限判定の根拠は Firestore のメンバードキュメントと本人参加状態に限定し、表示名・アバターは派生表示として扱うことで、プロフィール取得障害を権限障害へ波及させずに済む。

また、購読エラー時に「前回値を維持するか消すか」は一律では決められない。権限の正本が失敗した場合は fail closed、付加情報だけが失敗した場合は正本を維持する、というデータの責務ごとの判断が必要だった。

旅行状態未確定時の集合場所購読を止め、旅行切替時のスコープ混在を防止

bugfix未リリース

旅行状態未確定時の集合場所購読を止め、旅行切替時のスコープ混在を防止

何を変えたか

Trip Supporter の集合場所購読で、旅行詳細の状態がまだ確定していない間は Firestore の集合場所を購読しないようにした。

一方で、旅行前・旅行中・旅行終了後に登録済み集合場所を閲覧できる既存仕様は維持するため、scheduled / active / ended の状態が確定した後は従来どおり購読する。

さらに旅行切替直後に、選択中の旅行IDだけが新しい旅行へ切り替わり、旅行詳細が前の旅行のまま残る短い期間があることを考慮した。集合場所機能では selectedTripDetail.id === selectedTripId を満たす詳細だけを使うようにし、前旅行の状態やオーナー情報を新旅行へ持ち越さないようにした。

リリース状況は 未リリース。PR #22 は main へマージ済みだが、アプリストア配布は未実施。

なぜ変えたか

旅行詳細の取得前にも集合場所購読が開始されると、アクセス条件がまだ判断できない旅行へ Firestore 読み取りを行い、permission error を発生させる可能性があった。

最初の修正では scheduled を購読対象外にしたが、それでは「旅行前でも登録済み集合場所を閲覧できる」という既存仕様を壊す。問題は「旅行開始前」であることではなく、「旅行状態が未確定なのに購読すること」だった。

技術的な内容

  • useMeetingPlaces は scheduled / active / ended のみ読み取り可能状態として扱う。
  • tripId が空、または status が未確定なら集合場所購読を開始しない。
  • 旅行切替時は、選択中IDと詳細IDが一致する場合だけ集合場所用の旅行詳細として採用する。
  • 同じスコープ判定を購読だけでなく、集合場所の編集権限、オーナー判定、メニュー、オーバーレイにも適用する。
  • A旅行からB旅行へ切り替えた最初の描画ではBを購読せず、Bの詳細取得後に購読を開始する回帰テストを追加した。

ハマったこと・学んだこと

複数の state が別々に更新されるUIでは、「値が存在する」だけでなく「その値が現在選択中の対象に属しているか」を確認する必要がある。

selectedTripId と selectedTripDetail を別 state で持つ場合、旅行切替直後には一時的に異なる旅行を指し得る。非同期購読や権限判定へ渡す前にIDでスコープを揃えることで、古い状態による誤購読を防げる。

また、不具合の表面的な条件が「旅行開始前」でも、仕様上は開始前閲覧が正しい場合がある。エラーを消すために機能ごと止めるのではなく、失敗が起きる状態遷移を特定する必要がある。

スケジュール画像共有を選択中のアプリカラーへ合わせた

fix未リリース

スケジュール画像共有を選択中のアプリカラーへ合わせた

何を変えたか

Trip Supporterの全日程画像共有で、共有入口、プレビュー、画像ヘッダー、日付カード、予定アイコンの固定ピンク色を動的テーマへ変更した。淡色パレットの主色上の文字も、テーマのonPrimaryへ合わせた。未リリースのPR #17にpushした修正である。

なぜ変えたか

PR #17のレビューで、PR #16のアプリカラー設定との統合時に共有画面だけ固定ピンクのまま残る指摘があった。通常一覧から日付カードと予定行を抽出した後にテーマ対応が入ったため、抽出先にもテーマの購読とスタイル生成を適用する必要があった。

ユーザーにとって何が良くなったか

リリース後は、選択したアプリカラーが共有画面と生成する日程画像にも反映される。7色のパレットごとに主色、枠、淡い背景、文字色、ヘッダー、予定アイコンを合わせる。

技術的な内容

  • PR #16のブランチを取り込み、一覧と共有の共通タイムライン、ノートの操作なしプレビューを保持して競合を解消した。
  • useThemedStylesとmemoizeThemeStylesでスタイルをテーマごとに共有する。予定アイコンはprimaryContent、画像ヘッダーはheader、主色上の文字はonPrimaryを参照する。
  • アイコンフォント準備APIにも選択中の描画色を渡し、固定ピンクの引数を除いた。
  • 全7パレットの共有UIと、マウント済みの通常一覧・共有表示の色変更をテストした。変更前の8件の失敗を確認後、修正して成功を確認した。

判断したこと・トレードオフ

動的テーマの実装を共有機能側で重複させず、PR #16へ依存させる。PR #17の比較元は一時的にcodex/app-color-settingsとし、#16マージ後はmainへ戻す。レビューでは共有機能の差分に集中できるが、マージ順に依存する。

ハマったこと・学んだこと

コンポーネント抽出とテーマ対応が並行した場合、元の画面の動的スタイルだけ残しても抽出先は色変更に追従しない。共有画像に使う子コンポーネントまでテーマを参照する必要がある。淡い主色の上に白文字を固定すると読みやすさが変わるため、背景色と文字色を同じパレットの組で選ぶ。

全日程のスケジュールを1枚の画像で保存・共有する

feature未リリース

全日程のスケジュールを1枚の画像で保存・共有する

何を変えたか

Trip Supporterのスケジュール画面に「スクショ・共有」を追加した。旅行の全日程を1枚の縦長PNGにし、生成画像のプレビューから写真保存とOS共有シートを使える。閲覧できる参加者全員が利用できる設計。PR #17で実装済み、未マージ・未リリース時点の記録である。

旅行名、期間、日付、時刻、予定名、場所、写真、既存ノートのプレビューを含める。操作ボタンや招待情報は画像に含めない。予定のない日も「予定はありません」と表示する。

なぜ変えたか

Yahoo!乗換案内のスクショ共有のように、旅行全体の予定を画像で渡したいという要望があった。表示中の画面だけを撮ると、全日程を伝えるために何枚もの画像が必要になる。

ユーザーにとって何が良くなったか

リリース後は、全体の日程を1枚の画像で確認してから、LINEなどの共有先へ渡せるようになる。端末の写真にも保存できる。予定の写真が読み込めない場合や画像が大きすぎる場合は生成を止めて理由を示し、不完全な画像を成功として共有しない。

技術的な内容

  • 通常一覧と共有画像の日付カード・予定行を共通化し、共有時だけ操作要素を除いた。
  • 非仮想化ScrollViewをreact-native-view-shotのcaptureRef、snapshotContentContainer: trueでPNG化する。
  • Modalの表示完了、コンテンツ寸法、全写真のonDisplay、アイコンフォント登録を待ち、さらに2フレーム待って撮影する。写真・フォント準備には20秒のタイムアウトを設けた。
  • 生成されたPNG自体をプレビューする。保存はexpo-media-library、共有はexpo-sharingを使う。写真追加権限だけを要求する。
  • React stateで表示を管理し、refで連打と非同期処理の世代を管理する。閉じる・アンマウント・遅れて返った生成結果のそれぞれで一時画像を解放する。保存・共有中はプレビューを保持する。
  • 旅行名と実在日付を検証し、期間内の空日も描画する。登録済みの期間外予定も切り捨てない。欠損データを固定値で補完しない。

Before / After

Before

全日程を画像でまとめて保存・共有する専用機能がなかった。

After

「スクショ・共有」から全日程PNGのプレビューを開き、「写真に保存」または「共有」を選べる実装になった。実機での確認とリリースはこれから行う。

判断したこと・トレードオフ

  • 1枚のPNGという要望に合わせて全内容を描画する。巨大な旅行ではメモリ消費が増えるため、1,600万ネイティブピクセル・最大辺32,767を上限にした。RGBAだけで約64MBになる計算を根拠とするアプリ側の制限であり、OSでの生成成功を保証する値ではない。
  • 読み込めない写真を省略した画像や縮小画像で失敗を隠す処理は追加しない。
  • ノートは一覧で使っているプレビューを含め、本文全体や予定詳細の予約番号等は対象にしない。
  • ネイティブModal内の保存結果は同じModal内の共通Toastに出す。背面画面のToastを使うとModalに隠れるため。

ハマったこと・学んだこと

  • view-shot 5.1.0のiOS実装はreleaseCaptureに裸のファイルパスを要求する。保存・共有に使うfile:// URIをそのまま解放へ渡さず、iOSだけスキームを除く。Android側はURIを受け取れることを導入済みソースで確認した。
  • アイコンのgetImageSourceをawaitするだけでは不十分だった。内部のフォント読み込みが失敗を握りつぶす場合があり、expo-font.isLoadedも確認する必要があった。導入済みソースに合わせ、登録名をAndroidではMaterialIcons、iOSではMaterialIcons-Regularとして検査する。
  • 写真の取得完了と実際の表示完了は異なる。expo-imageのonDisplayを各予定の一意キーで追跡することで、画像の表示を待って撮影する。
  • SDK境界をサービスへ集約して動的importにした。コンポーネントテストはネイティブ境界をmockし、非同期状態・連打・閉じる操作を検証する。

旅行の状態にかかわらず通知設定を反映するようにした

improvement未リリース

旅行の状態にかかわらず通知設定を反映するようにした

何を変えたか

Trip Supporter の通知対象を見直し、旅行が開始前の scheduled、旅行中の active、旅行終了後の ended のいずれでも通知を生成するようにした。

対象は次の通知。

  • スケジュール更新
  • 精算確認
  • チャットメッセージ
  • 次の予定

削除済みの deleted 旅行は引き続き通知しない。

なぜ変えたか

従来はチャットなどの可変通知を active の旅行だけに限定していた。そのため、旅行開始前に参加者同士で相談するためチャットを使っても、メッセージ自体は保存される一方で相手の端末へ通知されなかった。

通知カテゴリごとの詳細設定が利用できるようになったため、旅行状態で一律に止めるのではなく、受信者が選んだ設定を優先する設計へ変更した。

ユーザーにとって何が良くなったか

旅行開始前の相談や旅行終了後の精算・連絡でも、新着情報に気づけるようになる。不要なカテゴリはプロフィール設定で個別にOFFにでき、予定単位の通知設定も引き続き利用できる。

技術的な内容

即時通知は既存の次の処理フローを維持した。

notificationEvents
  → Cloud Functionsで受信者を展開
  → userNotifications / notificationQueue
  → FCM / APNs

Cloud Functionsの旅行状態判定を「active だけを許可」から「deleted だけを除外」へ変更した。旅行状態を厳格に検証した後、受信者ごとに以下を判定する。

  • 操作した本人ではないか
  • 旅行を非表示にしていないか
  • 対象カテゴリの通知設定がONか
  • 旅行メンバーから削除されていないか

次予定通知の定期処理は、Firestoreの旅行検索条件を active 単独から scheduled、active、ended の in クエリへ変更した。その後にプロフィールの次予定通知設定と、予定単位の通知設定を両方確認する。

Before / After

Before

  • scheduled の旅行では、チャットを送信してもpush通知を生成しない
  • ended の旅行では、精算やチャットの通知を生成しない
  • ユーザー設定がONでも旅行状態による抑止が優先される

After

  • scheduled、active、ended の全状態で通知を生成できる
  • プロフィールの4種類の通知設定を旅行状態にかかわらず反映する
  • 予定保存時と予定単位の通知設定も維持する
  • deleted の旅行は通知しない

判断したこと・トレードオフ

  • 状態判定を完全に削除せず、削除済み旅行からの誤通知を防ぐため deleted の除外は維持した
  • 終了済み旅行でも通知するため、過去の旅行で連絡や精算を続ける場合には便利になる一方、不要な通知はユーザー設定で止める必要がある
  • 既存の通知配信経路は変更せず、配信対象判定だけを変更して影響範囲を限定した

ハマったこと・学んだこと

  • FCMやAPNsにエラーがなくても、通知キューが作られる前のポリシー判定で受信者が0件になると、端末には何も届かない
  • 配信障害の調査では、イベント生成、受信者展開、キュー生成、FCM送信を段階ごとに確認する必要がある
  • ユーザーが詳細設定を持つ場合、システム側の一律な状態制限と設定内容が競合しないように優先順位を明確にする必要がある

集合場所の現在地制御と同時編集後の復旧を修正

bugfix未リリース

集合場所の現在地制御と同時編集後の復旧を修正

何を変えたか

Trip Supporter の集合場所機能で、本人の共有状態が存在しない場合にも現在地を利用できていた処理を修正した。sharing / stopped を確認した場合だけ許可し、null・不正値・読取失敗をクライアントとサーバーの両方で拒否する。場所検索と手動のピン配置はこの確認に依存しない。

同時編集の競合後、同じフォームに「最新内容を読み込む」を表示する。入力の破棄を確認してからサーバー限定で再取得し、最新のリビジョンで再編集できる。機能は未リリースで、本番デプロイとアプリ配布は未実施。

なぜ変えたか

共有状態の欠損をゴーストモードOFFと推測すると、現在地利用の条件を確認できない。編集競合では古いリビジョンを繰り返し送っていたため、入力を変えても保存できない状態になっていた。

ユーザーにとって何が良くなったか

位置共有の状態が確認できないときに現在地を使わない。競合後もフォームを閉じずに復旧でき、最新内容の読み込みに失敗した場合は入力を保持する。

技術的な内容

  • RTDBの共有状態は明示的な許可状態だけを採用する。未確認状態を許可状態として扱わない。
  • 競合を検知したら保存を止め、確認モーダルの後にFirestoreから再取得する。
  • 既存の互換層にサーバー限定取得を追加し、getDocFromServerへ委譲する。通信断時にキャッシュから最新内容を補わない。
  • 新リビジョン、削除状態、権限、旅行状態を確認してフォームを更新する。旅行切替後の遅い応答は破棄する。
  • 再開後は新しい操作IDを使い、通常の通信再試行では同じ操作IDを維持する。

ハマったこと・学んだこと

Firestoreの既定取得はサーバー未到達時にキャッシュを返す。編集元より新しいキャッシュでも、サーバーの最新状態とは限らない。競合復旧で入力を置き換えるときは、サーバー限定取得が必要だった。

単に古いリビジョンを最新値へ差し替えると、別メンバーの変更を知らずに上書きする。最新内容を明示的に読み直してから編集する流れを用意した。

インタースティシャル広告の表示機会とプリロードを見直した

improvement未リリース

インタースティシャル広告の表示機会とプリロードを見直した

何を変えたか

TripSupporter のインタースティシャル広告について、表示間隔と表示タイミングを見直した。

  • デフォルトの最低表示間隔を3分から2分へ変更
  • アプリ起動時に広告SDKを初期化し、インタースティシャル広告を事前ロードする既存経路をテストで固定
  • 予定削除成功後を広告表示候補に追加
  • 割り勘支出の追加・更新成功後を広告表示候補に追加
  • 旅行チケット有効中は従来どおり広告を表示しない

なぜ変えたか

従来は広告を表示する操作が少なく、広告SDKの初期化・ロード状態やユーザー操作の頻度によっては、広告表示機会がかなり少なくなる設計だった。

一方で、チャット送信や位置情報更新のような高頻度操作に広告を入れると操作体験を損ねるため、DB更新が正常終了した後の「作業が一区切りつく操作」に限定して表示候補を増やした。

ユーザーにとって何が良くなったか

広告表示機会は増えるが、最低2分の間隔制御を維持し、旅行チケット有効中は広告を出さないため、連続表示や旅行中の操作妨害を抑えられる。

技術的な内容

広告表示は各画面で直接AdMob SDKを操作せず、共通の showInterstitialAd() を経由する。

showInterstitialAd() 側で次を判定する。

  • チケットが有効か
  • 前回広告表示から最低間隔が経過しているか
  • SDK初期化済みか
  • インタースティシャル広告がロード済みか

起動時は共通のアプリ初期化処理から initializeAds() を呼び、SDK初期化後に loadAd() で広告を事前ロードする。

表示候補を追加する場所では、DB操作成功後に fire-and-forget で広告表示を試行し、広告のロード失敗などが本来の操作結果へ影響しないようにした。

Before / After

Before

  • デフォルト最低表示間隔: 3分
  • 主な広告候補: 旅行作成・更新・参加・終了、予定保存
  • 起動時プリロードは実装済みだったが、直接の回帰テストが不足していた

After

  • デフォルト最低表示間隔: 2分
  • 予定削除、割り勘支出追加・更新を広告候補へ追加
  • 起動時プリロードをテストで固定
  • 旅行非表示は、対象旅行と現在選択中旅行のチケット状態が一致する保証が弱いため広告候補には採用しない

判断したこと・トレードオフ

  • 広告頻度を単純に増やすのではなく、操作完了後の自然な区切りだけに限定した
  • 旅行非表示も候補にしたが、広告判定に使うチケット状態が対象旅行とずれる可能性があるため採用しなかった
  • バナー広告の常時表示は行わず、既存のインタースティシャル広告だけを利用する
  • 最低表示間隔を短くすると収益機会は増える一方、ユーザー体験への影響も増えるため、2分のスロットリングを残した

ハマったこと・学んだこと

  • 広告表示条件は「その画面でチケットが有効か」ではなく、「広告対象となる操作の旅行でチケットが有効か」を一致させる必要がある
  • 表示トリガーを追加するときは、広告SDKの条件だけでなく、操作対象と状態のスコープが一致しているか確認する必要がある
  • 起動時プリロードは既存実装に存在していたため、同じ処理を重複追加せず、テストで仕様を固定する方が安全だった
  • UIコンポーネントへ新しい必須Propsを追加すると、実装だけでなく既存のテスト用Propsも更新する必要がある。CIのTypeScriptチェックで漏れを検出できた

チャットの拡張メニューから集合場所を共有・管理する機能

feature未リリース

チャットの拡張メニューから集合場所を共有・管理する機能

何を変えたか

Trip Supporter のチャットに、カメラ横の「+」から機能を選ぶ入口を設けた。今回の対象は「現在地を集合場所にする」と「地図から集合場所を選ぶ」。集合場所は旅行ごとに複数登録でき、登録・変更・削除をチャット履歴と連動させた。

リリース状況は未リリース。公開済みの機能として紹介しない。

なぜ変えたか

自由入力のメッセージだけでは、集合する地点とその変更履歴を一覧で確認しにくい。位置を共有する操作と集合場所を管理する操作を、旅行単位で扱いたいため。

ユーザーにとって何が良くなったか

現在地から指定する方法と、場所検索や任意の地図ピンで指定する方法を選べる。場所を設定するとチャットにも通知するため、マップを開いていないメンバーにも変更を伝えられる。

技術的な内容

集合場所の更新とチャットメッセージの保存を、一つの操作として扱う。片方だけが成功しないよう、場所、チャットの内容、ルーム件数、通知イベントを同一トランザクションで保存するCallableを実装した。操作IDを固定して再試行し、二重タップや再送でカードを増やさない。

現在の集合場所と、チャット送信時の場所の情報を別に保持する。過去のメッセージの座標を新しい座標に書き換えず、変更済み・削除済みという状態だけを表示する。

メッセージは既存のテキスト種別で操作内容の本文を保存し、追加の構造化情報を新アプリがカード表示に使う。旧アプリも同じ本文を読める構成にした。不正な構造化情報を通常本文表示で隠すことはしない。

Before / After

Before

チャットとスケジュールの地点は利用できるが、集合場所専用の登録・履歴管理はない。

After

集合場所は複数登録でき、マップとチャットから閲覧できる。登録・変更・削除は旅行中だけ可能で、変更・削除は登録者本人と旅行オーナーに限定する。終了後も閲覧できる。

判断したこと・トレードオフ

  • 定型メッセージは今回の対象から外し、集合場所の管理に範囲を絞った。
  • ゴーストモードでは現在地からの設定を禁止する一方、検索・手動のピン配置は利用できるようにする。
  • 集合日時は任意で表示のみ。リマインダーは作らず、通知は既存のチャット設定に従う。
  • 名前はフォームに「集合場所」を入力済みにするが、保存済みデータの欠損をこの文字列で補わない。

ハマったこと・学んだこと

  • 現在地取得に失敗したとき、最後に取得した座標を返す既存実装は、「今いる場所」を集合場所にする用途には使えない。新しい位置の取得成功と過去の位置を区別する必要がある。
  • ユーザーの画面でボタンを無効にするだけでは、編集権限や旅行終了後の更新を防げない。サーバーでも状態と権限を検証する。
  • チャットに送った過去の位置と、最新の集合場所を分けることで、変更前後の地点を追える履歴を維持できる。
  • 地図の初期カメラ設定は非同期で取得した地点の更新に追従しない。地点変更でカメラを更新し、名前やメモ変更ではユーザーの表示範囲を戻さないようにした。
  • 現在地を再取得するときは、以前の現在地を無効化する。再取得失敗後に古い座標を保存できる状態を回帰テストで防いだ。
  • 集合日時と表示タイムゾーンを保存し、夏時間で存在しない時刻・重複する時刻を明示的な入力エラーとして扱う。
  • ゴースト状態はRealtime Database、保存はFirestoreのため、両DB間の完全な原子性は持てない。取得前後と保存時に状態を確認する。

アプリカラーを選べる設定と落ち着いた7色のパレットを追加

feature未リリース

アプリカラーを選べる設定と落ち着いた7色のパレットを追加

何を変えたか

Trip Supporter の主色を設定画面から選べるように実装した。ピンクを初期色に、ブルー・セージ・ラベンダー・ピーチ・サンド・ブルーグレーを用意した。追加6色は落ち着いた淡色で、淡い背景上の文字とアイコンには濃い色を使う。

この変更は 未リリース。PR #16 に実装をまとめた段階で、ストア提出・本番配布はしていない。アプリ内お知らせは公開していない。

なぜ変えたか

ピンク固定だったアプリに、利用者が好みに合わせて色を選べる機能を追加したかった。鮮やかな色だけでなく、落ち着いた色・薄い色も選びたいという要望があった。

ユーザーにとって何が良くなったか

リリース後は、プロフィール保存と独立してアプリカラーを選び、ボタン・ヘッダー・強調表示・関連背景を変更できる。端末内に保存するため、次回起動も選択色を維持する。端末間の同期は行わない。

技術的な内容

  • 主色、濃いアクセント、主色上の文字、ヘッダー、淡い背景、枠、影をパレットとして管理した。
  • AsyncStorage の読込値を許可された7つのIDとして実行時検証した。未設定だけを初期値ピンクとして扱い、不正値・読込失敗は専用のエラー状態にした。
  • 起動時はカラー読込完了まで画面を表示せず、一瞬ピンクが出てから保存色へ変わる状態を防ぐ構成にした。
  • useSyncExternalStore によるテーマ購読を導入し、色変更で画面やFirebase購読を再マウントしない構成にした。
  • テーマはIDごとに共有し、スタイルはテーマオブジェクトをキーとする WeakMap で共有した。
  • ホーム・マップ・スケジュールの大きなスタイルファイルを責務別に分割した。
  • 警告・エラー・削除の意味色、写真上の白文字、メンバー識別色は維持した。

Before / After

Before

主色はピンク固定。共通テーマの参照だけでなく、画面内の固定ピンクや静的スタイルにも色が分散していた。

After

設定から7色を選択し、保存成功後に既存画面へ反映する。不正な保存値は再読込または明示的なピンク設定で復旧でき、保存失敗は選択中の色を維持して共通Toastで伝える。

判断したこと・トレードオフ

  • 端末単位の設定にし、アカウント同期やFirestoreスキーマ変更は追加しなかった。
  • 淡色の背景だけを変更すると白い文字・アイコンが読みにくくなるため、背景と前景を一組で定義した。
  • 色変更のためにナビゲーションへkeyを付けて再マウントする方法は採用しなかった。
  • 起動失敗をピンクで隠さず、利用者が再試行または初期色を選べる状態にした。

ハマったこと・学んだこと

  • import時に生成されるStyleSheetは、テーマ定義を変更しただけでは更新されない。コンポーネントのテーマ購読とスタイル生成の両方が必要だった。
  • ヘッダーだけでなく、選択アイコン、チケットのおすすめバッジ、写真追加ボタン、認証リンクも前景の色を確認する必要があった。
  • 写真上の白文字まで淡色用の濃い文字に変えると別の問題になるため、表示背景の役割を分ける必要があった。
  • 既存テストには静的スタイルの参照同一性を使うものがあり、テーマごとのスタイル共有で実行効率と参照の安定性を保った。
  • ワークツリーの全件テストにはGit管理外の既存iOS設定が必要だった。設定ファイルはローカルで用意し、コミットしなかった。

記事公開基盤

Markdownを正本として、Qiita・Zenn・chicchi.workへの記事公開と、Buffer経由のX告知を管理する仕組み。

公式ページ

Java ThreadLocalの値持ち越しと公開手順に関する技術記事を追加

docs未リリース

Java ThreadLocalの値持ち越しと公開手順に関する技術記事を追加

リリース状況

全媒体の公開完了は未確認(未リリース扱い)。2026-11-04の記事は作業ブランチで作成し、PR #187のTest publishing scripts成功後にmainへマージした。Qiita同期はGitHub Actionsで成功し記事IDを保存、BufferでXへの投稿作成記録も保存した。Zenn公開用PR #188はCIの後にマージされ、zenn-deployに記事が存在することを確認した。ただし、この記録時点ではQiitaとZennの実ページのHTTP 200・タイトル一致を独立に確認できていないため、全媒体の公開完了とはしていない。

何を変えたか

  • articles/2026-11-04-java-threadlocal-threadpool-cleanup.mdにJava 17向けの技術記事を1本追加した。
  • スレッドプールでThreadLocalを片付けないと後続タスクに値を持ち越す例と、try-finallyでremove()を呼ぶ対策を掲載した。
  • 例外が発生しても値を片付けられることと、入れ子スコープでは無条件のremove()に注意が必要であることを説明した。
  • 現行READMEに従い、mainへの直接pushではなくPR・全記事検証・マージを行った。

なぜ変えたか

ThreadLocalは「リクエスト単位の変数」と誤解されやすいが、実際にはスレッドに値が紐付く。スレッドを再利用するサーバー処理では、過去の処理の追跡情報を誤参照する可能性がある。再現できる短いコードを用いて、データの寿命とタスクの寿命の違いを説明するため。

ユーザーにとって何が良くなったか

Javaの並行処理に不慣れな読者でも、値が残る条件と安全な後片付けを具体的に試せる。バージョンを固定した公式APIの出典と実行コマンドを付け、実装・レビュー時の確認点として再利用できる。

技術的な内容・検証

  • Oracle Java SE 17のThreadLocal、Executors、ExecutorServiceの仕様を一次資料として照合した。
  • OpenJDK 21でjavac --release 17によるコンパイルとJavaコードの実行を確認した。
  • newSingleThreadExecutor()で同一ワーカーを再利用し、remove()未実行時の値の残存、正常終了・例外時の削除を確認した。
  • Zennは正本mainとzenn-deployを分け、公開済み記事を保護しながら未公開候補を1本ずつ投影する方式を維持した。Zenn側のページ確認が取れるまでは候補を確定済みとしない。

判断・ハマりどころ

  • ThreadLocalの値を別スレッドへ暗黙に伝播させるのではなく、必要な情報はメソッド引数へ明示的に渡す方法も併記した。
  • ThreadLocal.remove()は現在のスレッドの値だけを消す。外部リソースのクローズやプール全体の終了とは別である。
  • GitHubへのマージ成功、QiitaのAPI処理成功、Zenn公開ブランチへの反映、実際のHTTP掲載確認を別々に扱う。公開サイトの確認不能を成功に読み替えない。

公開フラグとニュースのコード例に対する検証を修正

bugfix未リリース

公開フラグとニュースのコード例に対する検証を修正

リリース状況

未リリース。ローカル実装後、独立レビューとPRのCIを経てmainへ反映する。本番手動確認は対象外。

何を変えたか

ニュースのpublishedと更新履歴のpublishableで、引用符付きの文字列を真偽値として受け入れないようにした。ニュースの本文からコードフェンスとHTMLブロックを除外し、その中の見出し・日付・一次情報を件数や必須欄として扱わないようにした。

なぜ変えたか

検証が通ってもサイトの厳密な真偽値比較で掲載されない入力と、Markdownのコード例を本物のニュースと誤認する入力があったため。

ユーザーにとって何が良くなったか

掲載されない公開指定を事前に検出でき、コード例が含まれるニュースの件数と必須情報を誤判定しにくくなる。合法な既存コンテンツの合格を全件検証で確認する。

技術的な内容

公開フラグの型チェックを共通化し、引用符・数値・null・継続行を拒否する。本文はfront matterを分離し、フェンスやHTMLブロックの行を空行に置き換えてから、既存の行頭ニュース書式で件数と各項目を検証する。投稿処理、X告知の時刻、Runner、既存記事・投稿IDは変更しない。外部依存は追加しない。

ハマったこと・学んだこと

値を検証する前に引用符を剥がすと、文字列と真偽値の区別が消える。また、件数だけコードを除外しても必須欄の探索が元の本文のままなら偽の日付・URLが検証をすり抜けるため、同じ処理済み本文を使う必要がある。

Codex Day 25:プラン利用枠増加とPro 200減枠撤回の一次情報を検証

docs未リリース

Codex Day 25:プラン利用枠増加とPro 200減枠撤回の一次情報を検証

リリース状況

未リリース(GitHub記事PR・外部公開の検証前)。 正本に技術記事articles/2026-10-30-codex-day25-usage-limits-doubled.mdを追加。mainへの反映は、最新PR headでの全件記事検証・公開スクリプトのテスト成功後だけ実施する。Qiita・Zennはマージ後の同一公開ワークフローで連携し、双方の実記事URLを個別に確認する。

何を変えたか

  • Day 25のTibo氏原文をOpenAI Developer Community引用と突き合わせ、JSTの日付・発表時刻を整理。
  • 2026-10-29付のOpenAI公式ChatGPT Release Notesと、更新されたProヘルプのプラン別詳細を照合。
  • Pro 100・500はWork/Codex枠が2倍。Pro 200は従来の高い枠は維持し、低い枠のみ2倍として統一と正確に記載。
  • Business Premiumの増枠、Plusの現状維持と、その他プランの不明点を区別。
  • GPT-6 ProのChat週間メッセージ上限は別体系で、Pro 100/500では変更されないと記載。
  • Dots自体の利用と、Dotsが起動するWork/Codexタスクの利用計上を区別。
  • Proプランの5時間枠、FastとUltrafastの消費率、API課金とサブスクリプション枠の相違、反映の確認方法を解説。
  • 一時的なフルリセットと恒常的な増枠を混同しない。

なぜ変えたか

公式のSNS投稿は「all usage limits 2X」と述べる一方、同じ投稿はPro 200の既存高枠を維持すると説明している。ヘルプ本文を参照しないと「Pro 200の全員が2倍」と誤解しやすい。また9月29日の旧減枠予告が古い翻訳・キャッシュに残る場合があるため、最新公式リリースの撤回を優先した。

ユーザーにとって何が良くなったか

各プランで誰が実際に増枠し、誰が現状維持なのかを把握できる。GPT-6 ProのChat制限、Dotsの会話、Work/Codex共有枠、API課金を別々に判断できる。速度モード・推論量を考慮して実務に必要な作業時間や上限を見積もれる。

技術的な内容

  • Day 25の発表時刻は2026年10月29日16:56 UTCで、日本時間10月30日01:56。
  • Pro 100/$100、Pro 200/$200、Pro 500/$500の月額は公式Proヘルプで確認。
  • Pro 100/500はWork/Codex allowanceが2倍、Pro 200高枠は維持、低枠は倍増で統一、Plusは変更なし。
  • Pro 100/200/500のWork/Codexでは現時点で5時間窓の上限はないが、他の利用枠はある。
  • 通常ChatのGPT-6 Pro週間メッセージ上限とWork/Codexの共有枠は別。
  • Fastのサブスク枠消費率は2.5倍、Ultrafastは8倍。今回のDay 25は係数変更ではない。
  • Dot自身の会話は通常Chat使用量とは別、Dotが起動するWork/Codexタスクは共有枠を消費。

Before / After

Before

Day 25の短い発表だけでは、Pro 200高枠が維持される理由や、増枠の対象機能・GPT-6 Proの通常Chat制限との違いが分からなかった。

After

最新の公式リリースノート、Proヘルプ、Codex利用量の公式説明を照合した長文記事で、プラン別の事実・不明点・利用上の判断を分けた。

判断したこと・トレードオフ

  • 公式な絶対利用量がない枠を、推定回数やトークン数で埋めない。
  • 高い枠だったPro 200まで2倍になったと書かず、減枠撤回を中心に説明する。
  • 例として使った100単位等の値は、すべて仮の数値であると明示する。
  • 日付・時間はUTCからJSTに換算し、企画の米国日付と日本での発表日を分ける。
  • QiitaのAPI投稿成功・Zenn公開用PRの成功・実際のZenn掲載は別々に検証する。
  • 既存のslugやQiita/Bufferの投稿ID、Zennの既存公開集合を変更しない。

ハマったこと・学んだこと

  • 英語Help Centerは2026年10月29日の減枠撤回・同一上限を反映する一方、古い翻訳・検索キャッシュには削減予告が残る場合がある。
  • Day 25の「2倍」は一律のチャット回数・タスク数・API従量課金の変更ではない。
  • Dotsの利用量の「別枠」は、Dots経由のWork/Codexタスクのコストがゼロという意味ではない。
  • Proの5時間枠がないという公式仕様と、週間その他の利用上限を混同しない。

Qiita 429時でもZennの同日公開を完了し、HTTP検証の一時障害を切り分けた

reliabilityリリース済み

Qiita 429時でもZennの同日公開を完了し、HTTP検証の一時障害を切り分けた

リリース状況

Zennへの同日公開はリリース済み・実掲載確認済み(2026-10-29)。 通常記事 articles/2026-10-29-postgresql-not-valid-constraint.md をPR #135の最新headでテスト成功後mainへマージ。その公開イベントでZennの生成PR #136を作成し、CI成功、信頼済みfinalizer成功、zenn-deployへのマージを完了した。Zennの「Verify Zenn publication」ワークフロー(run #40847210748、再実行2回目)は 74/74記事を公開確認し、今回の記事もHTTP 200、URLとcanonicalおよびh1タイトルの一致を確認した。

Qiitaは未リリース(未公開)。同じ公開イベントから新規POSTを試みたが、Qiita APIがHTTP 429(一定時間内の新規投稿数制限)を返した。記事IDは未登録のまま、Xへの自動告知もスキップしている。制限を避けるための一括再送はしていない。

何を変えたか

通常記事を作成し、レビュー済みの二媒体公開フローを本番で利用した。公開ワークフロー自体は改修していない。公開結果の相違と検証中の一時エラーを確認し、未完了は既存のバックログIssue #70に残した。

なぜ変えたか

従来、Qiitaが429になった際にZennまで止めたり、GitHubへのマージを外部公開完了と誤認したりする問題があった。改修済みの「Qiita状態保存後、Zennを独立起動」の運用が、実際のAPI失敗条件でも機能するか観測する必要があった。

ユーザーにとって何が良くなったか

Qiita側の投稿制限が発生しても、Zennへの初回公開を翌日の復旧キューまで待たせずに実行できた。媒体ごとに実URLの結果を分けて報告でき、未公開記事を公開済みとして扱わない運用を確認した。

技術的な内容

  1. PR #135の Test publishing scripts が最新head 116d073ba903a2487c4fe196ab695495b07116b6 で成功し、mainへマージ(1c8225a83ac6c75cef16816be5081a4c1d6fe90d)。
  2. Publish articles and news のpublish-qiitaはQiita API 429で失敗。しかしpublish-zennは公開前のvalidationと対象選択が成功していたため起動した。
  3. Zenn生成PR #136では今回の記事1本のみを追加し、既存の公開記事を維持。PRのtest成功後、notify-queueから信頼済みfinalizerを起動し、zenn-deployにマージした。
  4. Zenn公開用ブランチの確認Workflowは初回にhttp.client.IncompleteReadで失敗。これはZennページの本文が不正であることを示すものではなく、HTTPレスポンスのチャンク読込中断による検証ツールの例外だった。
  5. 読取専用の失敗ジョブのみ再実行したところ、今回の記事を含む全74記事が公開検証を通過。再投稿・レートリミット回避は行っていない。

判断したこと・トレードオフ

  • Qiitaの429直後に何度も新規投稿せず、既存の1記事ずつの回収運用に任せる。
  • Zennについては今回の公開用PRを1記事に限定し、先行未確認候補の保護や24時間の再試行間隔を緩めない。
  • 検証に失敗しただけで記事本文を再投稿せず、まず原因(HTTP通信中断か、404やタイトル不一致か)を切り分ける。
  • 公開Workflow全体の失敗と、媒体ごとの実掲載は別の状態として記録する。

ハマったこと・学んだこと

scripts/verify_zenn_publications.pyのcheck_articleはOSError等を捕捉するが、http.client.IncompleteReadはhttp.client.HTTPException系なので現在の例外処理で拾えない場合がある。実際にresponse.read()で未捕捉例外が発生し、レポートJSON生成前にジョブが失敗した。

今回は読取専用のワークフロー再実行で解消したが、一時的なページ読込中断を記事の404と同一視しないこと、およびHTTP読込例外を記事単位の検証失敗に変換して有限回再試行する設計が今後の改善候補となる。修正コードは未実装であり、検証スクリプトの耐障害性改善は未リリース。

Codex Day 24のTexting記事の作成と公開管理

docsリリース済み

Codex Day 24のTexting記事の作成と公開管理

リリース状況

リリース済み(2026-10-29、GitHub・Qiita・Zenn)。 最新mainのREADME/AGENTSを実際に取得してから記事を執筆。PR #131の最新headで公開前全件検証・回帰テストが成功し、mainにマージ(コミット e873111bf16a6c5cde7c0805f1e52cead24a4069)。Qiita公開Workflow #40813022427のpublish-qiitaジョブが成功し、記事ID 29efd4697733474d1655 を.qiita/items.jsonに保存。BufferへのX告知も投稿作成成功(Xでの最終配信は未確認)。同じ記事公開ワークフローのpublish-zennジョブがZenn用PR #132を自動作成し、公開前CI成功後にzenn-deployへマージ。Zenn検証Workflow #40813534888は73/73記事すべて実公開を確認し、Day 24の本文も検証対象に含まれた。QiitaとZennは両方公開済み。chicchi.workの公開ページは別途未確認。

何を変えたか

  • Day 24の正式発表「Text your dot」を公式Release Notes・ChatGPT Learn・Dots Help Centerで調査。
  • 米国Pro向け限定ベータという利用条件と、日本での提供時期未発表を強調。
  • iMessageとRCS、写真・音声メッセージ、Dotからの能動的な通知、共有メモリ・プラグインを整理。
  • 電話番号の確認、Dot連絡先の保存、iMessage/RCS有効化の接続手順を説明。
  • Siriの既存のメッセージ送信機能とDotの連絡先の組み合わせを説明し、専用Siri API追加と混同しない。
  • 第三者メッセージプロバイダーLinq、本人のみの1対1通信、機密情報への配慮、承認リンクとSTOPを紹介。
  • Day 23のリセットは公式完了報告が2026年10月28日15:29JST頃にあったことを追記。

なぜ変えたか

Day 24の「メッセージでDotを使える」は、Dots本体が日本で使えるユーザーにも直ちに提供されるとは限らない。Dots本体の提供範囲とTextingの米国Pro限定ベータを誤解せず、利用方法とセキュリティ上の意味を整理する必要があった。またDay 23は完了報告が出ており、前記事の「完了未確認」から状態が更新された。

ユーザーにとって何が良くなったか

同じDotの記憶をChatGPTとメッセージアプリで共有する意味や、連絡先・Siri・通知の使い方を理解できる。利用資格がない状況で無駄な設定変更を試さず、情報を送る際のプライバシー上のリスクや重要操作の承認を確認できる。

技術的な内容

  • 日米時差:2026年10月28日のPDTはUTC−7、JSTはUTC+9。Day 24投稿は日本で10月29日9:49頃。
  • Texting対応はiMessage・RCS、形式はテキスト・画像・音声。グループチャット、電話、FaceTimeは未対応。
  • 米国のProユーザー向け限定ベータ。Business・Enterprise向けには今回提供されていない。
  • OpenAIがLinqを第三者通信基盤として利用。全経路で同一のE2EEが保証されるとの根拠はないため断定しない。
  • メッセージでの承認依頼はChatGPT内で確認。STOPでDotからのテキストを停止できる。
  • Day 24の新規グローバルリセット告知はなし。Day 23の完了報告と利用者個人の枠確認は別。

Before / After

Before

Day 24の短いX告知のみでは、対応プラン、対応地域、操作可能なチャンネル、認証と第三者通信処理を誤解する余地があった。

After

OpenAI公式の仕様を照合した日本語技術解説として、導入・制限・使い方・権限設計・適用条件を整理した。

判断したこと・トレードオフ

  • 実機テストしていない内容は実測・動作保証として書かない。
  • 「Siriから送信できる」をSiri専用APIの提供と取り違えない。
  • Dotの能力とメッセージチャンネルの制限は別の情報源で確認する。
  • Qiita投稿成功、Zenn専用ブランチへの登録、Zennでの実掲載を区別し、各媒体のURLを確認した。
  • README/AGENTS最新版を記事作成前に取得。本文追加はmain直接pushを行わずPR・CI成功を経てマージする。

ハマったこと・学んだこと

  • DotへのTextingはiMessage/RCSを使うため、一般的なSMS・WhatsApp・LINEの対応と混同しない。
  • Dot自体はPro以外の一部プランでも提供されるが、今回のTextingベータ対象とは一致しない。
  • 外部事業者の通信基盤を利用する場合、機密情報の共有やプラグイン権限を確認する必要がある。

README確認必須化・Qiita/Zenn公開連動のリリース確認

reliabilityリリース済み

README確認必須化・Qiita/Zenn公開連動のリリース確認

リリース状況

設定・実装はリリース済み(2026-10-28)。 PR #118の最新head 27bd86f3961e4e9afcd979c18f76862a92ed4975 に対する公開前の自動検証が成功した後、mainへマージした。マージコミットは af28e269734bd861a97311bc08ccb38bde2f5e99。main上の公開ワークフローにpublish-zennジョブが存在することを再取得して確認した。

この新しい流れでの実記事の初回公開は未実施。 次回以降に作成する記事から適用するため、今回ダミー記事や過去記事を投稿していない。各媒体の実掲載は次回の公開時に個別に確認する。

本記録は、2026-10-28-readme-first-dual-publication.md の実装時点の「未リリース」記録に対する、後続のリリース確認である。過去の記録は残す。

何を変えたか

READMEとAGENTSに記事を書く前の最新版確認を必須化した。記事のmain反映後は、同じ公開ワークフロー内でQiita処理の状態保存後にZennへの公開用PRを開始する。公開前の検証が成功していれば、Qiita APIの失敗だけを理由にZennはスキップしない。

なぜ変えたか

従来、Zennだけが夜の定時キューを待つため、QiitaとZennを記事作成後に同じ公開作業で扱う運用になっていなかった。READMEの確認と実装の入口を一致させた。

技術的な確認

  • PR #118のtestジョブはsuccess、Validate content and scriptsもsuccess。
  • 新規記事の即時候補化、旧バックログを混ぜない対象選択、既存公開記事の更新、下書き・予約・ニュース除外を検証。
  • 失敗記事の再試行間隔・最大回数・先行候補保護、公開snapshotの改ざん拒否、CIとfinalizerの整合性を検証。
  • mainにpublish-zennジョブとprepare --action publishが反映されたことを読み取り確認。
  • 記事本文、slug、Qiita/Buffer ID、Zennの公開済み集合は今回のPRで変更していない。

判断したこと・ハマりどころ

Qiita/Bufferの投稿状態保存がmainを更新するため、Zennの公開snapshot生成をその後に置いた。「同じ公開契機」は両媒体の掲載完了時刻が同じ秒になるという意味ではない。初回送信を即時化しつつ、失敗済み記事の連続再送や大量の未公開記事の一括送信は行わない。

README確認の必須化と記事作成に連動したQiita・Zenn公開

reliability未リリース

README確認の必須化と記事作成に連動したQiita・Zenn公開

リリース状況

未リリース(作業ブランチに実装、PR検証前)。 この変更は次回以降の記事作成・公開に適用する。記事本文や既存の投稿IDは変更していない。既存記事の一括再投稿、サービス側の制限解除、新規の定期タスク追加は行わない。PR最新headの自動テスト成功後にmainへマージする。実記事の公開完了は次回の媒体別検証で判定する。

何を変えたか

  • READMEとAGENTSに、記事を書く前に毎回最新のREADME・AGENTSを確認するルールを明記。
  • 記事作成、PR検証、mainへのマージ、QiitaとZennの両方の公開処理、実掲載確認を一連の作業に定義。
  • Publish articles and newsにpublish-zennジョブを追加。Qiitaの投稿ID・Buffer状態保存後に最新mainを取り直し、同じ記事公開イベントからZenn公開用PRを作成する。
  • Qiita APIやBufferの失敗だけではZennをスキップしない。公開前検証・対象選択の失敗では停止する。
  • Zennにpublishモードを追加。今回の追加・更新または明示的な手動公開で指定した記事だけを選択し、初回送信は夜の定時や直前の記事の24時間待ちにしない。
  • 公開用PRの生成・同head CI・mainの信頼済みfinalizer・実ページ検証は維持。
  • 失敗済み記事の再試行間隔、最大回数、単一の未確認候補、公開済み記事の保護は維持する。

なぜ変えたか

従来はmainへの記事追加でQiitaが投稿される一方、Zennは毎日20:40の別キューまで開始されなかった。記事を書いた直後に両方の公開を進める運用と一致していなかった。また会話履歴だけを参照して古い公開設計を説明する問題を避けるため、最新版READMEを毎回読むことを運用上の必須手順にした。

ユーザーにとって何が良くなったか

通常の新しい記事は、Qiitaの処理を行った同じ公開作業の中でZennへの送信も開始される。どちらかが失敗しても他方の結果を隠さず、掲載完了を媒体ごとに確認できる。次回からZennだけを夜の定時まで待たせることを通常運用にしない。

技術的な内容

publish-zennはQiitaジョブの終了後に動くが、実行条件はジョブ全体のsuccessではなく、公開前検証と対象選択のsuccess、および未キャンセル・main・push/手動公開である。これはQiitaの状態保存がmainを更新してZennのsource SHA検証を無効にする競合を避けるための順序であり、外部サイトの掲載完了時刻をそろえる仕組みではない。

Zennの対象選択は元のpushのbefore/after、または手動公開のarticle入力から行う。ニュース、削除済みファイル、下書き、未来予約を除外し、過去の未投稿全件を暗黙に追加しない。選択したパスを公開snapshotへ記録し、CIとfinalizerが同じ入力から出力を再計算する。

新しい記事の初回送信と、失敗済み記事の再試行を区別した。新規は未試行かつ先行未確認候補がなければすぐ送信する。再試行は24時間・各2回の条件を保持し、複数の未公開記事を同時に渡された場合も一括送信せず停止する。通常記事は1記事ずつPRにする。

Before / After

Before

記事main反映 → Qiita公開。Zennは別の20:40キューまで開始しない。READMEの確認が記事作成の最初の必須作業になっていなかった。

After

README/AGENTS確認 → 記事PR・テスト → main反映 → 同じ公開ワークフローでQiitaとZennの両方を処理 → 各媒体のURLと状態を確認する。20:40の処理は復旧用に残る。

判断したこと・トレードオフ

  • 外部サービスへの同じ秒の掲載は保証せず、公開開始の契機を同じ記事公開作業に統一する。
  • Qiitaによる状態更新とZennの検証競合を避けるため、独立ジョブだが実行順序を付ける。
  • 未公開の先行候補、未完了PR、mainの変更、認証・ネットワーク異常では安全に停止し、未公開を成功扱いしない。
  • 記事を公開して検証するためだけのダミー記事は投稿しない。外部書き込みを行わない回帰テストを追加する。
  • 既存の単一候補方式を保持し、今回の作業で古い未公開記事を一括送信しない。

ハマったこと・学んだこと

  • 同じmain pushで2媒体を並列に開始すると、片方の投稿ID保存によってもう片方のsource SHAが古くなることがある。公開イベントを共有しても、状態保存とsnapshot生成には順序が必要。
  • GitHub Actionsのneedsだけでは先行ジョブ失敗時に後続がスキップされるため、!cancelled()と検証ステップの結果で実行条件を明示する。
  • 初回送信の即時化と、429回避のための再試行抑制は別の条件にする必要がある。
  • CI成功・PRマージ・キュー投入は、外部サイトで記事が表示されたこととは異なる証拠である。

Codex Day 23リセットとiCloud連携記事の出典・公開ステータス

docsリリース済み

Codex Day 23リセットとiCloud連携記事の出典・公開ステータス

リリース状況

GitHub原稿とQiita投稿はリリース済み。Zennへの配信は未完了、自サイト掲載は未確認。 2026-10-28のPR #113(コミット8fbbc32034d8f28fafb2f1dfd77d8998d42a1820)で、記事の公開前テスト成功後にmainへマージした。当初Qiita公開Workflow #40638180750は新規投稿時にHTTP 429で失敗したが、同日18:55 JST頃に失敗したジョブ1件だけを再実行(run_attempt=2)し、Qiita API投稿とID保存に成功した。記事URLは https://qiita.com/sponge841841/items/e7eaa2343139211ab790 で、.qiita/items.jsonにも対応表が保存された。BufferへのX告知は投稿作成成功(X側での最終配信は未確認)。Zennはzenn-deployの専用キューで管理され、Day 23は今回の確認時点では専用公開ブランチへ登録されていないため、Zennに公開済みとは扱わない。

何を変えたか

  • Day 23のTibo Sottiaux氏によるリセット発表と返信「Lands by midnight PST」を日本時間付きで整理。
  • PSTを標準時として読む場合と、米国西海岸の実際の夏時間PDTを意味すると読む場合の1時間差を説明。
  • リセットの発表・適用・全体への完了報告・個別アカウントの実測を区別。
  • 自動・保存型・購入型リセット、次回回復日時の変更、UsageとCLIの確認方法を公式ヘルプに基づいて記載。
  • iCloud Mail・Calendarプラグイン自体は、同じ日にOpenAI公式が正式リリースしたと確認。Tibo氏が冗談にしたのは「これをDay 23の28日企画の成果とする」という部分。
  • MailとCalendarを別個に接続すること、Apple専用アプリパスワードが必要なこと、権限・承認の注意点を説明。

なぜ変えたか

Day 23の冗談を字義どおりに受け取り「iCloud連携は新機能として公開されていない」と断定すると誤報になる。OpenAI公式リリースノートの2026年10月27日GAと、Tibo氏のDay 23リセット投稿は別の事象であり、それぞれの一次情報で確認する必要がある。

ユーザーにとって何が良くなったか

  • Day 23で何が本当に提供されたかを誤解せず把握できる。
  • iCloudプラグインの利用可能なプラン・端末、別個の接続手順、アプリ専用パスワードの注意点を理解できる。
  • リセット予告が確認できても、自分のUsageに未反映なら手動で保存型リセットを消費しない判断ができる。
  • 重要な送信・予定編集の承認などを考慮できる。

技術的な内容

  • 公式X投稿の時刻は2026-10-28 02:42:53 UTC、返信02:43:38 UTC。JSTは+9時間。
  • 2026年10月27日の西海岸の現地時間はPDT(UTC−7)、投稿中のPST(UTC−8)との時差解釈で日本時間16:00/17:00の幅が生じる。
  • iCloudの製品発表はOpenAI Release notesで2026-10-27、ChatGPT Help Centerでも同日付として記載。
  • iCloud MailはiCloud専用のメールアドレス、CalendarはApple Accountの主メールアドレスを使用。両者は別のプラグインで、Appleのアプリ専用パスワードで接続。
  • Zennへの公開は現在、mainの原稿と別のzenn-deploy専用公開キューで管理。mainマージだけではZennでの実掲載を意味しない。

Before / After

Before

短いDay 23投稿を読むと「iCloud連携は冗談なので未提供」と誤解する可能性があった。反映期限の日本時間への換算もPST/PDTで曖昧。

After

Day 23リセット発表と実在するiCloud連携の公式GAを、別の発表として扱う記事に整理し、時刻・仕様・使用手順・安全面を出典付きで説明。

判断したこと・トレードオフ

  • Day 23のリセット全体完了報告と個別反映を、実測されるまで完了と断定しない。
  • iCloudの利用可能な全端末を拡大解釈しない。公式はWeb・デスクトップ・iOSを明記。
  • Apple Accountのメインパスワードをチャットへ貼るよう案内しない。
  • Day 20〜22の比較は必要最小限とし、Day 23固有の時差・冗談・実際のiCloudリリースを中心に記事を構成。
  • 公開済みのDay 20〜22記事本文、Qiita ID、slugを変更しない。

ハマったこと・学んだこと

  • Xの一次投稿は取得制限があるため、原文アーカイブと時刻付き記録で照合した。OpenAIの機能仕様は公式リリースノート・Help Centerで確認。
  • Tibo氏の原文は「iCloud Main」、公式プラグイン名は「iCloud Mail」で、綴りが異なる。引用では勝手に書き換えない。
  • リセットと製品リリースが同じ日に起きても、同じ施策とは限らない。
  • 利用枠のリセットを示す宣言は、対象アカウントの実際の使用状況確認とは異なる。

Day 22 GPT-6.1 Terra記事の一次情報検証と公開管理

docs未リリース

Day 22 GPT-6.1 Terra記事の一次情報検証と公開管理

リリース状況

未リリース(記事のPR作成段階)。 articles/2026-10-27-codex-day22-gpt61-terra.md をブランチ上に作成した。PRの全件記事検証と公開スクリプトの自動テストに成功した後、mainにマージする。Qiita、Zenn、自サイトへの公開はそれぞれ独立して確認し、掲載が確認できない媒体は未公開と明記する。

何を変えたか

  • Day 21のリセットとDay 22の新モデル発表をまとめた、主にDay 22を解説する技術記事を追加した。
  • APIモデルID、標準入力・出力・キャッシュの単価、最大コンテキスト、推論設定、画像入出力の制限をOpenAI公式仕様で照合した。
  • Agents' Last Exam、OSWorld、Terminal-Bench 4.0、誤り率の数値を評価条件と一緒に説明した。
  • APIにcurlおよびJavaのHttpClientからアクセスする実装例を載せた。
  • Day 21の既存記事を参照し、同じ内容の重複記事は作成しない。
  • 前回短い回答で示した「Terraの利用枠はSolの約2倍」という固定倍率は一次情報で確認できなかったため、正しい公式表現に修正した。

なぜ変えたか

新モデルの「料金が半額」「仕事が速い」「同じ利用枠で多く作業できる」という主張はそれぞれ別であり、混同すると誤ったコスト試算やモデル選択につながる。特にトークン単価が半額であることからWork/Codexの作業可能量を一律2倍と推測するのは不適切であるため、公式表現と計測条件を分離した。

ユーザーにとって何が良くなったか

  • Terraの得意な作業とSol・Astraを選ぶべき複雑な作業の境界を理解できる。
  • 価格表と具体的なコスト計算でAPI導入時の比較を行える。
  • Computer Use実装時の権限・監査・人間による承認の必要性を把握できる。
  • Day 21のリセットとDay 22のモデル効率を別の仕様として理解できる。

技術的な内容

公式モデル仕様では、IDはgpt-6.1-terra、コンテキストは1,050,000トークン、最大出力は128,000トークン、推論レベルはnone〜max。API標準料金は入力$1、キャッシュ済み入力$0.05、出力$5(各100万トークンあたり)。入力が272Kトークンを超える長文処理は割増対象。

Terminal-Bench 4.0はmax設定で旧Terra 21.5%に対して新Terra 50.3%。Agents' Last Examはmedium設定で旧Terraより15パーセンテージポイント改善と公式が報告。これらを実環境全体の成功率とは扱わない。

APIのサンプルはPOST /v1/responsesを使い、reasoning.effortにmediumを指定した。Javaの標準HttpClientの例はtry-with-resources使用時にJava 21以上が必要という注意点を明記した。

Before / After

Before

Day 22の短い発表と料金の概要は把握できたが、ベンチマーク・API仕様・利用効率・安全設計がまとまった日本語記事になっていなかった。

After

公式資料を相互照合した技術記事にまとめ、コーディング・画像・GUI操作の評価や実装例を提示した。

判断したこと・トレードオフ

  • Day 21のリセット詳細は既存の独立記事を正本とし、Day 22記事は新モデル中心にした。
  • 料金・コンテキスト・ベンチマークなど時期で変わり得る数字には公式URLを付けた。
  • Computer Useの「モデルができること」と「実行環境が付与した権限」を混同しない。
  • リセット告知と全アカウントの反映完了、ChatGPT Work/Codexの利用枠とAPI課金を区別する。
  • published: trueは公開意思表示に過ぎず、Qiita API成功・Zennページ確認は別工程とする。
  • タイトルのUTF-16長70以内、slug12〜50文字を維持し、PRチェック成功後にmainへマージする。

ハマったこと・学んだこと

  • 「同じ利用枠で多くの仕事をこなせる」は「固定2倍」と同じではない。一次情報にない倍率を削除する必要がある。
  • 推論レベルが異なるベンチマーク数値を同じ条件の結果として比較しない。
  • Terminal-Benchの成功率は開発現場全体のバグ修正成功率ではない。
  • Java 17のHttpClientはAutoCloseableではないため、try-with-resourcesの例を使うならJava 21以降と明記する。
  • Zennの公開用ブランチがzenn-deployとなっており、mainの新規記事が直ちにZennへ配信されるわけではない。

正本とZenn公開集合を分離する単一候補キュー

feature未リリース

正本とZenn公開集合を分離する単一候補キュー

リリース状況

未リリース。ローカル実装と回帰テストまで。独立レビューとコードPR最新headCI後、非記事bootstrap・同期branch切替・既存30公開維持確認を経てmainへ反映する。confirmed-only集合と限定移行試行の本番結果は別途確認する。

何を変えたか

main正本を変更せず、Zenn専用zenn-deployのarticlesに確認済み29件とactive未公開高々1件だけ出力する。待機原稿・下書き・予約未到来はfalseでも出さず物理的に含めない。削除済みlegacy公開1件は保護URLとして確認し再投稿しない。

なぜ変えたか

Zennデプロイ通知で、1ファイルだけ空白変更してもmain内の未公開37件すべてが新規投稿候補になると判明した。差分1件と投稿候補1件は異なるため、同期先へ渡す集合自体を分離する必要があった。

ユーザーにとって何が良くなったか

Qiita/chicchi.work向け正本の公開設定を変えずZennだけを制御できる。公開確認前に次候補を加えず、公開済みは除外/false化しない。通常の公開済み編集もreview済みmainから同じ生成PRへcopyする。

技術的な内容

公開inventoryは単調増加。全sourceとlegacyをlive確認し、既存公開の404や429/通信/認証/タイトル不一致等では処理を停止する。同head Test publishing scriptsが正本全件とsubsetの厳密な投影を検査し、main信頼コードがlive再照合後に通常mergeする。

通常1件/24時間/2回。旧Day19のattempt1/日時をリセットせず初期confirmed-onlyへ引継ぐ。今回だけ、旧state全一致・candidateなし・Day19試行1・migration_done:falseを条件に同じDay19を試行2/現在時刻で1回実証する。markerは不可逆で一般cooldown回避や自由候補指定はない。Zenn拒否なら停止する。

ハマったこと・学んだこと

  • 下書きfalseも新規作成候補になり得るためファイル集合で制御する。
  • mainだけのinventoryでは外部に残る削除済みlegacy記事を見落とす。dashboard全一覧の公開30/下書き1とHTTPを照合した。
  • subset rootに正本用ID検証を適用せず、同headCI内でsource全件検証とsubset投影検証を分ける。
  • main先行mergeも旧Zenn同期を起動するため、非記事branchへ先に切替えて既存公開維持を確認する段階移行が必要。
  • Git Trees APIはworkflow権限なしでworkflowファイルを書こうとすると404を返し得る。初回非記事branchは既存Git認証と新規ref限定leaseで準備し、通常更新は親treeと非workflow差分だけを使う。workflow変更があれば別途レビュー済み更新が必要として停止する。

Zennの投稿候補を本番で1件に限定した結果

featureリリース済み

Zennの投稿候補を本番で1件に限定した結果

リリース状況

単一候補キューはリリース済み。実装PR #83とディレクトリ補修PR #85をmainへマージし、Zenn同期先をzenn-deployへ切り替えた。記事公開の復旧は未完了。Day 19だけを本番へ送ったが、Zennの投稿数上限で拒否された。

何を変えたか

mainを記事の正本として保持し、Zennへ渡すファイルを確認済み29記事と未公開候補高々1記事に限定した。正本から削除済みの公開記事1件は再投稿せず、公開URLを保護対象として確認する。待機原稿、下書き、公開予約前の記事は出力へ含めない。

なぜ変えたか

従来は1ファイルだけ変更しても、同期対象main内の未公開37記事すべてをZennが新規投稿候補として扱った。変更差分だけで投稿数を制御できなかったため、同期先のファイル集合自体を分離した。

ユーザーにとって何が良くなったか

公開済み記事を維持しながら、未公開記事を1件ずつ扱える。Zennが拒否した候補の公開確認が取れるまで次の記事を追加しない。mainのQiita向け設定や既存IDを変更せずにZennの投稿対象を制御できる。

技術的な内容

生成PRの最新headで正本全件と出力集合を検証し、main上のfinalizerが公開状態・正本・親コミットを再照合して通常マージする。初回の非記事ブランチ準備、同期先切替、確認済み集合の展開、候補1件の追加という順序で移行した。

本番でbooksディレクトリ不足の通知も確認したため、空のbooks/.gitkeepを常に出力するよう補修した。ディレクトリ補修だけの処理では新しい候補や再試行を追加しない。

本番確認の事実

  • PR #84で確認済み29記事を展開。公開URLは管理外の既存1記事を含む30/30件で確認成功。
  • PR #86でbooks/.gitkeepを補修。記事差分なし、候補なし、旧Day 19の試行1回を維持。検証40275256614は30/30件成功。Zenn最新通知からディレクトリ警告が消えた。
  • 限定移行試行40275726471がPR #87を生成。記事差分はDay 19の新規1件だけで既存29記事は変更なし。同headのCI40275811566が成功し、finalizer40275868440を経て9e7b3159c37642537673676d3739b20d76d893e7へ自動マージされた。
  • Zennの当該コミット通知は「投稿数の上限」の対象として2026-10-24-codex-day19-cli-0164-cacheだけを列挙した。以前の37件一括列挙は解消した。デプロイ成功という表示だけで記事の公開成功とは判断しない。
  • 公開検証40275971536は有限回の再試行後に失敗となり、JSONレポートは31対象中30件確認成功、Day 19だけHTTP 404だった。既存30件は維持され、候補の公開失敗を成功扱いしないことも確認した。
  • 旧試行をリセットせずDay 19は試行2回、migration_done=trueとなった。次候補を追加せず、この候補を保持する。
  • 本番コミットを取得し、公開URLを再確認して通常計画を読み取り専用で生成したところ、出力は現在のブランチと完全一致した。既存30件の公開維持と、次候補・追加再試行・state-onlyデプロイが生成されないことを確認した。追加の本番ワークフロー実行は行っていない。
  • 親エージェントの全回帰テスト188件、全記事・ニュース・プロダクト検証と独立レビューを実施した。Qiitaとchicchi.workへの公開は今回の本番確認対象外。

ハマったこと・学んだこと

GitHubのマージ、Zennのデプロイ成功表示、記事の公開成功はそれぞれ異なる。GitHub上のファイル集合だけでなく、Zenn通知に列挙された実際の候補と公開URLを確認する必要がある。

Zenn復旧CIからの自動マージを本番で確認

reliabilityリリース済み

Zenn復旧CIからの自動マージを本番で確認

リリース状況

自動マージ修正のコードと実行経路はリリース済み・本番確認済み。 PR #81を独立レビューと最新head必須CI成功後にmainへ反映し、既存PR #80の同じDay19記事を最新mainへ安全更新した。同head CI成功、callbackによる明示dispatch、finalizer成功、PR #80の自動マージ、全公開記事Zenn検証の起動まで本番で確認した。

Zenn上のDay19公開は未完了。 自動マージ後の全公開記事検証は最大8回・20秒間隔で有限に確認したが、2026-10-26 10:21:54 JSTの保存JSONではDay19もHTTP404だった。全体は66記事中29件公開確認・37件HTTP404未確認でworkflowは失敗を記録した。HTTP404だけから投稿数制限等の原因は断定しない。追加再同期や別記事追加は行っていない。Qiitaとchicchi.workの公開成否は今回未確認。

何を変えたか

  • PR #81でworkflow_runへの暗黙依存を撤去し、CIのcallbackからmainの専用finalizerへrun ID/attemptを明示dispatchする構成へ変更。
  • 既存復旧PRだけを再開する手動resumeと毎時reconcileを追加。新しい記事は選択しない。
  • PR #80を元head一致を条件に安全更新。最新mainのDay19本文から空白toggleのみ再生成し、metadata/body/slug/公開IDは保持した。
  • 選択Day19と試行1は維持し、準備時刻だけ2026-10-26T01:14:46.125363+00:00へ更新して、停滞前の古い時刻が24時間保護を短縮しないようにした。

なぜ変えたか

元の復旧CIは成功したのに後続workflow_runが発生せず、PR #80がopenのまま残っていた。APIのname/display_titleが動的run-nameになっており、固定workflow名filterと異なるという実測を得た。イベント連鎖の制約にも依存せず、成功したCIから後続を明示起動する必要があった。

ユーザーにとって何が良くなったか

CI成功後に自動マージが始まらない問題を解消し、既存PRの安全な再開と本番実行を追跡できる。マージ成功とZenn公開成功を分けて報告する。

技術的な内容

本番の実行順序は次のとおり。管理者バイパス、mainへの直接push、PR #80の手動マージは行っていない。

  1. 修正PR #81: head 9c01804b1fc21c50d5d0cabc5239b75231c1dbc9 の必須CI 40253064495 成功後、main dd4b92016e323dca81781a2fd556d652fb4b4482 へ通常マージ。
  2. PR #80の最新候補: mainの唯一の子 ffcd794acedb014e5da515579ca0afba664f8ba2、同じ2ファイルのみ。独立レビューと通常PR CI 40253446024 成功。
  3. 既存PR専用resume 40253538848: 成功。新記事を選択するrecover jobはskip。
  4. 同headの復旧CI 40253559109: test/notify-recovery両jobが成功。callbackがGITHUB_TOKENでfinalizerを明示dispatch。
  5. finalizer 40253601884: mainの信頼済みコードがCIと差分を再検証して成功。PR #80を2026-10-26T01:19:02Zに自動マージし、merge commitは 7e161296513ed8c6189bd92107f8ae1ee2567d66。
  6. 全公開記事のZenn検証 40253630952: 自動マージ後mainを対象にworkflow_dispatchで起動。有限確認後はfailure、66記事中29件確認・37件HTTP404でDay19も未公開。他の通信エラーはなし。

mainに保存されたstateはDay19の試行1のみ。他の記事や別の試行回数は追加していない。通常testはread権限を維持し、追加したactions:writeはcallback jobに限定した。設定の権限拡大やPAT追加は行っていない。

ハマったこと・学んだこと

  • 静的workflow名と動的run-nameを混同すると、完了したCIを後続イベントfilterが捕捉できない。
  • 単体テストだけではGitHub側のイベント連鎖を実証できない。実際のGITHUB_TOKEN callbackから後続runが起動することを確認する必要がある。
  • 単一runnerをCI待機に使わず、job終了後に別workflowを実行する。終了ステータスの反映猶予は有限10秒とし、CI実行本体は待たない。
  • 古いPRを更新するときは、古い原稿を上書きせず最新mainの内容から差分を再生成する。長時間停滞した準備日時も実際の再同期時刻へ更新する。

Zenn復旧CIから自動マージを明示的に再開する

reliability未リリース

Zenn復旧CIから自動マージを明示的に再開する

リリース状況

未リリース。ローカル修正と回帰テストまで実施し、独立レビュー・修正PR・CI・main反映はこれから。今回はPR #80の同じDay19記事について、本番CIから自動マージとZenn公開検証まで確認する予定。コードの反映とZenn上の公開成功は別々に確認する。

何を変えたか

  • 暗黙のworkflow_runイベント依存を除去し、復旧CIの後段callback jobからmain上の専用finalizerへrun IDとattemptを明示dispatchする。
  • 通常のPR/test jobはread権限を維持し、actions:writeはmainの信頼済みコードを実行するcallback jobだけへ追加する。
  • finalizerはCI全体の完了成功、同repo/workflow/専用branch/識別子/head/attempt、PR、記事空白と状態差分をAPIから再検証する。終了反映の猶予は最大10秒で、CI実行そのものをrunner上で待たない。
  • 既存復旧PRだけを再開する手動resumeと毎時reconcileを追加。新記事の選択は1日1件のprepareに限定する。
  • 同headのCI実行中は重複開始せず、失敗したCIの再検証は明示的な手動resumeに限定する。古いmainのPRは停止し、内容不変のrebaseと再CIを求める。

なぜ変えたか

PR #80は自動作成され、そのCI 40205093149 は成功したが、自動マージ側のworkflow_run実行が発生しなかった。APIではname/display_titleの両方が動的な publication-recovery-40205054522-1 で、既存の固定workflow名フィルターと異なっていた。GitHub公式runnerリポジトリにもcompletedイベントでrun-nameがnameに使われる報告がある。GITHUB_TOKENのイベント連鎖制約も含め、暗黙イベントや表示名フィルターへ依存しない仕組みに改めた。

ユーザーにとって何が良くなったか

CIが成功したのにマージ処理が始まらない状態を検出・再開できる。未完了PRがある間に別の記事へ復旧対象を増やさず、同じ記事の検証結果を追跡できる。

技術的な内容

GITHUB_TOKENで明示起動できるworkflow_dispatchを使い、単一self-hosted runnerでcallbackが終了してからfinalizerが実行される。finalizerの成功後は従来どおり全公開記事のZenn確認を明示dispatchする。重複通知は再マージせず、1記事/24時間/2回の上限と本文・slug・メタデータの不変条件を維持する。

ハマったこと・学んだこと

  • 静的なworkflow名、run-name、APIのname/display_titleが常に同じ意味で扱われると仮定してはいけない。
  • CIの成功と後続workflowの起動は別々に確認する必要がある。モックの単体テストだけではGitHub側のイベント連鎖は実証できない。
  • CIを待つjobが唯一のrunnerを占有すると循環待ちになる。明示dispatch後にrunnerを解放し、取りこぼしは既存PRのreconcileで補う。

Codex Day 21リセット記事の作成・配信状況

docsリリース済み

Codex Day 21リセット記事の作成・配信状況

リリース状況

GitHubへの記事反映・Qiitaの新規公開はリリース済み(2026-10-26)。Zennは未公開、自サイトは未確認。 PR #89の最新コミットで公開前の全件記事検証と回帰テストが成功し、mainへマージ(コミット d94f6a52e745731bb6495374b6056147fcb033c3)。Qiita投稿Workflow #40283847195は成功し、記事ID ec2a0a45f81967476f9d を.qiita/items.jsonへ保存した。X告知はBufferで投稿作成成功したが、X実配信とは区別する。Zennは専用ブランチの単一候補公開キューで管理され、Day21記事はまだ掲載確認できていない。自サイトのHTTP公開状態も未確認。

何を変えたか

  • Day 21の公式X原文「But today is a reset」を日米の時差付きで整理。
  • Day 20は公式完了報告あり、Day 21は公式発表と一部個別観測があるが全体完了の公式宣言は未確認と区別。
  • PC Watchの筆者個人による適用観測と、OpenAIのグローバル配信完了を混同しない記事構成にした。
  • OpenAI公式リセットヘルプからautomatic / banked / purchasedの相違、週間回復日時の変更、モバイルで使えないリセット管理画面、CLIの/usageを整理。
  • Day 17〜20からの重複説明を最小化しつつ、Day 21の発表の特徴を中心に比較した。

なぜ変えたか

短い公式投稿だけではリセット完了時刻や対象者の条件を断定できない。個人のアカウント観測を全体の公式完了と誤認すると、誤った記事になるため「公式発表」「反映を目撃した利用者」「公式完了宣言」の各段階を分離した。

また、現行のZennは投稿数制限のため、専用の単一候補公開キューを用いている。記事がmainへ追加されたことだけではZennで公開したことにならないため、外部公開の実測を待つ。

ユーザーにとって何が良くなったか

Day 21の発表とDay 20の完了報告の違い、リセット券を誤って使わずUsageを確認する方法、週間枠の回復日が変わる可能性を1つの記事で把握できる。

技術的な内容

  • 記事のtitleはZennの70 UTF-16コード単位以内、slugは12〜50文字の範囲。
  • 公式X投稿の2026-10-26 05:21 UTCを2026-10-26 14:21 JSTと換算。
  • Qiitaへの投稿はarticles/*.mdのmainへのマージ後にGitHub Actionsが処理し、.qiita/items.jsonへ成功した記事IDを保存する。
  • Zennの現行構成は.zenn/published-inventory.jsonと別の単一候補公開キューで掲載を管理する。mainに記事があるだけではZennの公開結果を意味しない。

Before / After

Before

Day 21の公式投稿が見つかった段階で、新規機能と対象枠・配信完了が不明なままだった。

After

Day 21で確認できること・できないことを区別し、対象者・反映の段階・確認手順・出典をまとまった日本語記事にした。

判断したこと・トレードオフ

  • 反映期限の告知がないDay 21に、前日の「By midnight PST」を転用しない。
  • ターゲットプランごとの回復条件が示されない以上「全プラン100%」とは書かない。
  • 国内メディアでの個人観測を出典とともに示すが、公式の完了宣言とは区別する。
  • 既存のDay 20記事とは別記事とし、同じslug・見出し・本文の複製をしない。
  • 出版がサービス側の投稿制限で拒否されたら未リリースとして記録し、連続再投稿を行わない。

ハマったこと・学んだこと

  • UTCの2026年10月26日05:21はJST同日14:21だが米国PDTでは前日22:21。企画日と日本での掲載日が異なる。
  • 宣言、サービス全体の反映、個々の利用者の成功観測は異なるレベルの証拠。
  • banked resetの適用対象と、Day 21の自動配布対象を混同するとプラン別の誤報につながる。
  • GitHubへマージされた記事がQiita、Zenn、chicchi.workで自動的にすべて掲載されるとは限らない。

Zenn未掲載記事を1日1件ずつ再同期する復旧ワークフロー

reliabilityリリース済み

Zenn未掲載記事を1日1件ずつ再同期する復旧ワークフロー

リリース状況

復旧ワークフローのコードはリリース済み(2026-10-25、PR #68を最新テスト成功後にmainへマージ)。一方、未公開36記事の外部公開は未リリース。 毎日20:40 JSTに1件まで再同期するGitHub Actionsの設定を反映したが、初回の実行とZenn側での反映成功は別の確認事項。Zennの投稿数上限解除は保証されない。

何を変えたか

  • scripts/zenn_backfill.py と .github/workflows/recover-zenn-backlog.yml を作成。
  • 毎日11:40 UTC(20:40 JST)に公開記事のZennページを読み取りで確認し、HTTP 404の1記事だけ再同期のためにfront matterの空白を書き換える。
  • 同時に複数記事を再投稿しないため、1回の実行で最大1件、24時間以内に再試行しているときは処理を見送る。
  • .zenn/backfill-state.json に対象・試行時刻・試行回数を記録して、同じファイルに対する短時間の重複再試行を防ぐ。
  • 各記事への試行は最大2回とし、それでも公開されない場合はそれ以上の盲目的な試行をしない。
  • スクリプトは公開済みのURL・タイトルを確認し、認証やネットワーク異常などHTTP 404以外の失敗では書き換えを停止する。
  • テストでメタデータ・本文の同一性、対象選択、時間制御、複数試行の上限、エラー時の停止を確認する。

なぜ変えたか

Zennのデプロイ通知で多数の記事が投稿数上限のため未掲載と判明し、一度に36件を再保存しても制限で拒否される可能性が高い。デプロイ可能か分からないまま大量のpushを送信する方法を避け、サービスの制限を尊重して公開実態を確認しつつ、再同期を段階的に試す必要があった。

ユーザーにとって何が良くなったか

  • Zennへの投稿数制限を無視した大量の同期を避けられる。
  • 未公開だけを選ぶため、公開済みの記事本文・slug・既存URLを維持できる。
  • 公開されない場合でもログと状態を残し、未反映を公開成功と誤報しない。
  • 直近のCodex Day 19・Day 18記事を優先し、以前のバックログも順番に確認できる。

技術的な内容

verify_zenn_publications.py の読み取り専用URL検証を再利用する。HTTP 200、正しいcanonical、現在のタイトルに一致するh1が揃ったものだけを公開済みとする。HTTP 404だけを未掲載候補とし、他のHTTPエラー・通信失敗が含まれた場合は安全側で止める。

選ばれた1ファイルについて、front matter終端の空行だけを反転させる。parse_frontmatterで処理前後のメタデータと本文が等しいことを保証し、slugとタイトルは変更しない。

GitHub Actionsで対象記事と状態ファイルを1コミットとしてmainへpushし、別ステップで選択記事のみ最大4回公開URLを確認する。GitHub ActionsのGITHUB_TOKENによるpushは別のGitHub Actionsワークフローを自動起動しない場合があるため、Zenn側の同期と実ページを別途確認する必要がある。

Before / After

Before

未掲載の記事が残っていても、次の通常pushで差分のなかったファイルはZennの同期対象にならなかった。まとめて再保存するとZennの投稿数上限でまとめて拒否される可能性があった。

After

未掲載を検出した場合に、単一ファイルに対し24時間以上の間隔を空けて再同期を試す。結果のHTTP確認で公開済みと未反映を区別する。

判断したこと・トレードオフ

  • 1日1件はこちら側の安全な運用値であり、Zenn公式が公表した上限値ではない。
  • 再同期してもZenn側が投稿上限を理由に拒否した場合、処理成功としない。制限を迂回する別アカウントや別slugは作らない。
  • ZennのGitHub連携は登録ブランチの変更からデプロイするが、実際の成功はZennの側の処理に依存する。
  • 1件ずつの復旧は時間がかかる。大量移行として妥当なら公式サポートへの制限緩和相談も併用する。
  • 記事の削除、URL変更、本文書換え、公開設定変更は行わない。
  • 2回とも404の場合はそのファイルへの自動再試行を止める。永続的に失敗を連打しない。

ハマったこと・学んだこと

  • GitHubへのcommit成功はZennの公開を保証しない。外部HTTP 200とタイトル一致まで確認する必要がある。
  • Zenn側の公開上限は非公開で、24時間の投稿数から算出されるため、GitHub Actions内で真の残り枠を正確に把握できない。
  • メタデータの意味を変えずにファイルの変更イベントを発生させるときは、検証前後のparse結果の同一性をテストする。
  • すでに公開済みの原稿を二重に作成しないよう、ファイル名(slug)を固定し同一ファイルの変更だけを同期する。

React定期更新とPNGプレビューの同値性について日曜記事を作成

docs未リリース

React定期更新とPNGプレビューの同値性について日曜記事を作成

リリース状況

GitHubのmainへの記事反映と紹介履歴への記録は完了。Qiita・Zennの外部公開は未完了(未リリース)。 2026-10-25に記事を作成し、mainのコミット8a1e652ebe691131bacc927cdee1c3f18900631dで反映した。全150件の回帰テストと記事・ニュース・プロダクト更新履歴の形式検証は成功した。

Qiitaの公開Workflow 40179403318では新規投稿にHTTP 429が返り、記事IDは保存されずBufferへのX投稿もスキップされた。Zennの公開検証Workflow 40179403419は8回確認して28/66記事を確認し、残る38記事はHTTP 404。本日の記事を公開済みとは扱わない。

何を変えたか

  • 他の既存記事とプロダクト紹介履歴を照合し、重複しないReact定期更新・Canvas PNGプレビューのちらつきを記事化した。
  • 公開可能なPointPiaの2026-10-25の更新履歴2件を一次資料として、原因・設計判断・サンプルコード・回帰テスト項目をまとめた。
  • React公式useEffect/useMemo、MDNのCanvas toBlobとBlob URL、Web Share APIの仕様で外部APIの挙動を確認した。
  • products/promotion-history.jsonにgame-score-managerの新しい記事・切り口を追加した。
  • GitHubへの記事追加と、Qiita・Zennの実際の外部公開確認を分けた。未公開の外部記事IDを推測で作成していない。

なぜ変えたか

日曜日はプロダクト開発の公開可能な技術的知見を記事化する運用としている。PointPiaの結果画像共有は本番リリース済みで、その後の定期同期によるPNGの再生成不具合も本番修正が確認されていた。単なる新機能紹介ではなく、Reactのオブジェクト参照と表示内容の差、非同期競合とURL寿命の扱いを一般化した。

技術的な内容と学んだこと

  • 定期取得で同一内容の配列を新しく作ると、ReactのObject.is依存比較では変更扱いとなり得る。小さく明示的な表示用モデルを基準に、意味的な変更のみを画像生成の依存とする。
  • Blob URLは元の画像を表示・保存する間は維持し、不要になった時点でrevokeする。古い画像生成の非同期完了は破棄し、表示内容が変わった直後に旧ファイルを保存可能として扱わない。
  • PNG化に成功しても、スコアの丸めや省略で数値の意味が変わる場合があるため、実際のPNGを検証対象とする。
  • 外部記事公開はGitHub Actionsのテスト成功と別に確認する。Qiitaの投稿数制限やZennの404をコード検証成功として隠さない。

Qiita・Zenn未公開記事を重複投稿せず復旧する運用

reliabilityリリース済み

Qiita・Zenn未公開記事を重複投稿せず復旧する運用

リリース状況

再投稿制御コードはリリース済み(2026-10-25、PR #67をテスト成功後にmainへマージ)。ただしQiita・Zennの未公開記事は未リリースのまま。 QiitaはDay 18の記事1件だけを送信することを確認したが、APIはHTTP 429を返した。ZennのDay 18再同期後の確認も28/64で、36件未確認。両サービスの投稿数制限そのものが解除されたわけではない。

何を変えたか

  • 未反映の記事をGitHubの正本とQiita記事IDマッピングで突き合わせ、Qiita記事ID未登録の公開記事5件を確認した。
  • GitHub ActionsによるQiita投稿で、通常のpushに合わせて未登録全記事をまとめて送信していた動作を修正した。
  • workflow_dispatch(GitHub Actionsの手動実行)は指定した1記事だけを同期する仕様に変更。未登録記事を暗黙的にまとめて再投稿しない。
  • schedule(時刻指定実行)で未登録記事から1件を選んで再試行する。初回設定は毎日09:20 UTC(18:20 JST)。
  • Zenn側はサービスの投稿数上限を超えて一括再送しないため、Day 18記事1件のfront matterに空行を加えて再同期対象とした。タイトル、slug、公開設定、本文は変更しない。

なぜ変えたか

10月24日までの公開記事でQiitaの新規投稿数制限(HTTP 429)が継続し、失敗した5件を通常のpushのたびに一括再試行する挙動が残っていた。これでは制限中に不要なアクセスを繰り返す。Zennの未公開記事はZennダッシュボード通知から「投稿数の上限に達したためデプロイされなかった」と確認済みであり、一括同期を繰り返すことは適切でない。

ユーザーにとって何が良くなったか

  • 1回の手動実行で意図しない5件以上の新規投稿が発生しない。
  • 過去の未公開記事を追加の定期的な1件ずつの再試行で復旧できる可能性がある。
  • Qiitaでの新規投稿と既存記事IDによる更新、ZennのGitHub連携の再同期を区別できる。
  • 各サービスが公開を拒否した場合は未公開として記録を維持できる。

技術的な内容

scripts/ci_publish.py の select_targets を変更した。

  • push 時:変更された記事のみ。未登録記事は追加しない。
  • workflow_dispatch 時:入力された記事パス1件のみ。
  • schedule 時:Qiitaの未登録の公開記事を名前順に並べた先頭1件のみ。
  • news/ は従来どおり独立した処理とし、Qiitaの失敗でニュースを巻き込まない。
  • Qiitaの実際のPOST/PATCHは既存の scripts/publish.py と送信前の識別コメント照合を維持。重複記事の作成に疑いがある場合は安全側で停止する。

日次1件という間隔は安全側の運用値であって、Qiitaが公式に発表した1日1件の制限ではない。投稿上限の解除時刻は不明。失敗した場合は投稿IDを新しく保存せず、次回の試行で再度照合する。アクセス過多や永久ループを避けるため、一度の実行で失敗記事を繰り返さない。

Zennは直近24時間の投稿数(予約投稿を含む)などから上限を判定し、その具体的な値は非公開。今回のfront matterの空行による再同期は検証であり、公開済みとは断定しない。

Before / After

Before

通常push時に変更記事と未登録記事の全件を追加し、投稿枠が回復していなくても一度に複数件の新規POSTを送った。手動再実行でも指定記事に他の未登録記事が暗黙的に追加された。

After

通常pushは変更記事だけ、手動実行は指定の1件だけ、定期処理は未登録の1件だけを試行する。APIエラーは成功にしない。

判断したこと・トレードオフ

  • 並列投稿・連続再試行・新しいアカウントの作成など、サービス側の投稿数制限を回避する処理は採用しない。
  • 日次1件の復旧は速くないが、不明な上限に対して過負荷を避けられる。
  • Zennの36件超の未公開記事を一括書き換えない。まず1件の結果とZennダッシュボード通知を確認する。
  • 記事の本文・公開設定・既存Qiita/Buffer ID・slugは維持する。
  • GitHub Actionsは実行されて初めて外部公開に進む。CI成功と外部サイトの公開成功を同一視しない。

ハマったこと・学んだこと

  • HTTP 429はサービスの投稿数制限であり、コードを再実行するだけで解決しない。
  • バックログの一括再試行をCIの通常pushに組み込むと、制限時には毎回同じ未公開記事を大量に送る。
  • GitHub連携型ブログの「mainへマージ」と「サービスへの同期」は別で、Zennダッシュボードのデプロイ履歴と個別記事URL確認が必要。
  • すでに投稿済みかもしれない記事はQiitaの識別コメントとIDマッピングを照合し、タイトルだけでは同一記事とみなさない。

Qiita/Zennの順次復旧修正をmainへ反映

reliabilityリリース済み

Qiita/Zennの順次復旧修正をmainへ反映

リリース状況

機能コードはリリース済み。 PR #71の最新head 80c434692c5f9574a61b1dc72fb210f0a9e9cf2a に対する Test publishing scripts 成功と独立レビュー完了後、2026-10-25にmainへマージした。マージコミットは bd5d524a64a06920672213f142553d00c027ace6。

Zennの自動復旧運用には設定待ちが残る。 ActionsによるPR作成許可はfalseで、設定変更の許可は未回答のため変更していない。この設定が禁止のままなら復旧PR作成を停止する。設定を変えずmain直接pushへ戻す処理はない。

実記事の公開成功は未確認。 ユーザー指定により手動動作確認・本番手動実行・記事の実公開は行っていない。Qiita、Zenn、chicchi.workの公開状態を、コードのmain反映から推定しない。

何を変えたか

  • 未来の予約記事をQiita/Xへ送信しないよう、Zennと同じJSTの公開日時判定を共有。
  • 対象なしの実行では一時ファイルの前回対象を再利用しないよう初期化。
  • Zenn再同期の空白のみ差分を、Qiita同期の対象から除外。
  • Zenn復旧要求を作業ブランチのPRにし、同一headのCI成功イベントから通常マージする構成へ変更。
  • 単一self-hosted runnerでCI完了待ちが循環しないよう、CI開始とマージ判定を別jobへ分離。
  • GITHUB_TOKENによるマージ後に全公開記事のZenn検証を明示dispatch。

なぜ変えたか

PR #67/#68のレビューで公開前チェックの不足と、予約日時・一時ファイルの扱いに問題が見つかった。既存の1記事ずつ・24時間間隔・各記事2回の復旧上限を維持して修正した。

ユーザーにとって何が良くなったか

CI未確認のmain更新や予約前の公開を防ぎ、コードの反映と外部公開の成功を区別できる。通信失敗、CI失敗、不正差分、権限不足は成功扱いにしない。

技術的な内容

workflow_dispatchでCIを開始した後にrunnerを解放し、workflow_runの成功イベントで処理を再開する。信頼済みmainコードからイベントとREST APIのrepo・workflow・識別子・head、PR、唯一親main、記事空白と試行状態の差分を再確認する。

ハマったこと・学んだこと

  • GITHUB_TOKENによるpush/mergeは、別のpush型Actionsを自動起動しない。CIと全公開記事確認は明示dispatchが必要。
  • runnerが1台の場合、別workflowの完了を同じrunner上で待ってはいけない。
  • PR作成権限はworkflowのスコープとリポジトリ設定の両方に依存する。

未公開記事の順次復旧をレビューし公開前の安全性を修正

reliability未リリース

未公開記事の順次復旧をレビューし公開前の安全性を修正

リリース状況

未リリース。ローカル実装とオフライン回帰テストまで実施。修正PRのレビューとCI、mainへのマージはこれから。手動動作確認と記事の実公開は依頼により実施しない。Qiita、Zenn、chicchi.workの公開状態は未確認。

何を変えたか

  • Zennの再同期要求を、main直接pushから作業ブランチ・PR・最新headの必須CI成功後の通常マージへ変更。
  • GITHUB_TOKENによるPRでは別ワークフローが自動開始されないため、専用識別子で Test publishing scripts を明示dispatchし、同一headの実行だけ成功判定する。
  • CI開始後はいったんrunnerを解放し、成功完了イベントの別jobでmain上の信頼済みコードを実行する。イベントのrepo・workflow・識別子・headとPRを照合し、差分が記事空白と試行状態のみであることも再検査する。
  • マージ後は Verify Zenn publication を明示dispatchし、公開対象の全記事を自動確認する。重複したCI完了イベントでは再マージしない。
  • PRのhead変更、検証元mainの更新、CI失敗、タイムアウト、既存の復旧PRがある場合は停止する。通常のmerge APIを使い、保護要件を迂回しない。
  • 共有ランナーの一時ファイルに古い選択結果が残っていても、対象なしの実行が前回記事を再利用しないよう初期化する。
  • Zenn再同期用のfront matter空白だけの差分は、Qiita同期対象から外す。
  • Qiitaの復旧対象選択、公開処理、X告知、Zenn検証で予約日時のJST判定を共有する。

なぜ変えたか

PR #67と#68のコードレビューで、PRを必須にするリポジトリ規則に反した直接更新、予約日時を無視するQiita同期、前回対象の残存を確認した。異なる公開先の復旧が相互に不要な同期を起動しないようにする必要もあった。

ユーザーにとって何が良くなったか

1件ずつ復旧する方針を保ちながら、CI未確認の記事更新や予約前の公開を防げる。未反映・通信異常・権限不足を公開成功として扱わない。

技術的な内容

Python標準ライブラリによるGitHub REST APIを追加。復旧対象と試行状態は同一コミットのPRに載せる。24時間間隔と各記事2回の試行上限、本文・メタデータの不変条件は維持する。

ハマったこと・学んだこと

  • GITHUB_TOKENはイベント連鎖抑止があるので、PRを作るだけではCI待ちを実装できない。workflow_dispatchと一意な実行名、head SHAを照合する。
  • self-hosted runnerが1台しかない場合、そのrunner上で別workflowの完了を待つと、待たれているCIを起動できない。処理をworkflow_runで再開する構成に分ける。
  • ActionsのPR作成許可は権限スコープと別にリポジトリ設定へ依存する。設定が禁止されている場合は明示的に失敗させ、main直接pushへフォールバックしない。
  • RUNNER_TEMPのファイルは常に空から始まると仮定せず、対象選択の前に削除する。

日次ニュースの一次情報取得と公開前監査を分離した検証

docs未リリース

日次ニュースの一次情報取得と公開前監査を分離した検証

リリース状況

調査補助ツールは未リリース。隔離した調査用ブランチでセルフホスト実行を検証したが、このツールや調査用ワークフローをmainや定期実行へ追加していない。本記録のstatusは調査補助ツールの状態であり、ニュース記事の公開状態とは別である。

何を変えたか

一次情報へのアクセスが失敗する調査環境を補うため、公開URLだけを読み取る取得処理を調査専用ブランチで実行した。候補収集と、原稿を固定した後の全件再取得を別の実行に分けた。記事の公開ブランチはmainから別に作成し、調査コードを取り込まない構成とした。

なぜ変えたか

検索結果の要約だけでは、提供開始日、対象地域、会社名と製品名の違いなどを検証できない。HTTP取得成功と本文の事実確認も別であり、取得処理を成功させるだけでは公開の根拠にならないため。

技術的な内容

明示したHTTPSホスト、公開IPアドレス、リダイレクト先を確認し、並列数・タイムアウト・応答サイズに上限を設けた。取得処理へ公開先の認証情報は渡さず、本文、メタデータ、取得時刻、内容ハッシュを短期保存する構成を検証した。

対象日はAsia/Tokyoで固定し、公式RSSや配信メタデータの時刻を日本時間へ換算した。ページ上の現地日付、障害発生日、プレスリリースの公開日、将来の提供開始日を別の情報として扱った。

ハマったこと・学んだこと

HTML内に別記事の構造化データや関連記事が含まれる場合、最初に見つかった情報が対象本文とは限らない。URLや記事識別子を照合し、本文の内容まで確認する必要がある。

再監査では、ゲーム更新の配信予定と公開済みの取り違え、専用アプリの要否、運営会社と製品名の混同などを原稿段階で修正した。形式検証ではこれらを検出できないため、本文の再確認を省略しない。

判断したこと・トレードオフ

一次情報の取得、編集上の採否、形式CI、サイト反映、SNS告知を独立した状態として扱った。記事更新時は既存の投稿台帳を保持し、同じ対象日の告知を重複送信しない。調査用ワークフローの成功を、ニュースの公開成功や本番機能のリリース成功として扱わない。

Codex Day 20のリセット完了記事:公式告知・適用・個別検証を区別

docsリリース済み

Codex Day 20のリセット完了記事:公式告知・適用・個別検証を区別

リリース状況

GitHubへの記事反映・Qiita APIの新規公開はリリース済み(2026-10-25)。Zennは未リリース、chicchi.workへの反映は未確認。 PR #75の公開前検証は成功し、mainへマージ済み(5c2d15fb488b8cfcc6c332c736377f9fbd55c011)。Qiita公開Workflow #40170228118は成功し、Qiitaの新規記事ID b2c00d9f0e6fc6afe858 を取得し .qiita/items.jsonへ保存した。Xへの告知はBuffer投稿作成まで成功したが、X本体の配信完了とは区別する。Zenn検証 #40170228048では公開対象65記事中28記事だけ確認、Day 20記事もHTTP 404(未公開)。chicchi.workはHTTP公開確認未実施。

何を変えたか

  • OpenAIの28日間アップデートDay 20「Rest and reset」を一次情報で追跡した長文解説記事を追加。
  • 2026年10月25日12:39頃の発表、12:40頃の「By midnight PST」、15:06頃の「It is all propagated.」を時系列で整理。
  • Day 20の公式完了報告と、個々のアカウントへの対象枠反映を区別。
  • PST/PDTの1時間の時差、5時間枠・週間枠、保存型・自動・購入型リセットの違い、次回週間回復日時の変動、Usageの確認手順を解説。
  • Day 17〜19との違い、エンジニアが再利用できる発表・配信・確認の3段階モデルを掲載。

なぜ変えたか

SNSの短いリセット告知だけでは「発表した」と「全対象に反映した」を混同しやすい。OpenAI Help Centerではglobal resetの対象範囲がプロモーションごとに異なること、保存型とは別であることが明記されているため、利用者が誤った前提で使用量を判断しないよう事実と留保を整理した。

また、過去にQiitaでHTTP 429、Zennで投稿数上限による未公開が発生しているため、原稿作成と実際の公開を別の状態として記録する。

ユーザーにとって何が良くなったか

  • 「公式完了報告あり」と「自分のアカウントの残量が戻った」を正しく区別できる。
  • 保存型リセットを確認のために誤って消費することを防げる。
  • PSTとPDTの違いで反映予定時刻を誤解しない。
  • Settings → UsageやCodex CLIの/usageから確認する手順を参照できる。

技術的な内容

Day 20記事は、外部公式X原文とOpenAI Help Centerの仕様に依存する時事解説である。仕様が公開されていない対象プランの細分化・内部ロールアウト構造・各アカウントの実測を推測せず、提供元の公式宣言と個別検証を区別した。

記事のfront matterはZennのタイトル70 UTF-16コード単位の制限とslugの12〜50文字制限を満たす。公開Markdownの識別子を変更せず、PR上のCI検証成功を経てmainへ反映する。

Before / After

Before

Day 20の短い発表・完了報告のみで、反映範囲や保存型との相違、実際のアカウント確認の注意点を同じ場所にまとめた記事がなかった。

After

プランごとに変わり得る範囲、使える操作、時刻の解釈、個人の検証に必要なステップを整理した独立の記事を追加する。

判断したこと・トレードオフ

  • 「all propagated」は公式の完了宣言として扱うが、全種類の制限の100%回復の証明とは扱わない。
  • 計算用の残量%は実測でなく説明例と明記した。
  • 保存型リセットのプラン別一覧を、Day 20の特典対象プランの一覧と混同しない。
  • 公開済み記事とのテーマ重複を避け、Day 20固有の完成報告を中心にする。
  • Qiita / Zennの実配信が失敗しても記事本文の品質チェック成功とは区別する。

ハマったこと・学んだこと

  • 投稿時刻を米国太平洋時間からJSTへ換算する場合、10月は実際にはPDT(UTC-7)でも公式投稿ではPST(UTC-8)と略記され、1時間差が発生する。
  • システムの「反映完了」と利用者側の実際の動作確認は違う。デプロイ・機能フラグ・非同期処理の技術記事にも使える3段階の表現。
  • 外部サービスの投稿枠や制限は公開基盤のCIテストでは解消しないため、実際の公開を別のチェックで判定する。

セルフホスト移行後の再実行と、外部サービスの投稿制限を分けて確認する

reliability未リリース

セルフホスト移行後の再実行と、外部サービスの投稿制限を分けて確認する

リリース状況

全記事の公開復旧は未完了(未リリース)。 再実行用PR #54はmainにマージ済みで、セルフホストの本番公開ジョブが動いたことは確認済み。DynamoDB TTL記事1件のQiita API投稿成功、記事ID保存、Buffer投稿登録を確認したが、残る3記事はQiitaの投稿数制限で未公開。

Qiitaの公開ページ自体は、この調査環境から独立したHTTP確認を完了できていない。API成功・保存済みIDと、公開ページの独立確認を区別する。Bufferの登録成功も、X上での配信完了確認とは区別する。

何を変えたか

PR #53で公開・Zenn検証Workflowのruns-onがself-hosted、Linux、X64に変更された後、最新mainから再実行した。SNS/SQS記事の末尾に空行を1行追加するだけのPR #54を作り、記事本文・slug・公開設定・既存の外部投稿IDは変更していない。PR最新コミットのTest publishing scripts成功と差分を確認してからマージした。

再実行コミットは 09fdae8e4f9e828ffabab7fef8b87e4453b16193。新規記事や定期スケジュールは追加せず、定期タスクも停止していない。

なぜこの経路を選んだか

GitHub ActionsのRe-runは、元のイベントと同じGITHUB_SHAおよびGITHUB_REFを使う。旧ジョブを再実行するだけでは、その後にmainへ入ったRunner設定を試したことにならない。今回利用したコネクタにはworkflow_dispatch操作がなかったため、最新mainを基点とする小さな記事差分をPRで取り込み、既存のpushトリガーを使った。

確認した結果

公開実行 40058100226 のジョブはself-hosted、Linux、X64のラベルで実際に割り当てられた。111件の回帰テストと記事・ニュース・プロダクト更新記録の検証が成功した。

既存の未マッピング記事回収により4記事が同期対象となった。

記事 Qiita Buffer
2026-10-21 DynamoDB TTL API新規投稿成功、ID a19e99570104ff8e5605 をmainに保存 投稿ID 6adc24c63d5b27036b656aec を保存
2026-10-22 Java BigDecimal HTTP 429、未公開 Qiita失敗によりスキップ
2026-10-23 Codex Day 18 HTTP 429、未公開 Qiita失敗によりスキップ
2026-10-23 SNS/SQS FilterPolicy HTTP 429、未公開 Qiita失敗によりスキップ

429の応答本文は、一定期間内の投稿数を制限しているため時間を空けて再試行するよう求める内容だった。制限解除時刻や具体的な投稿数上限は応答から確認できていない。

Zennの最終確認

今回起動した検証実行 40058100292 は、後続の別更新によってキャンセルされた。その後の最新mainに対する検証実行 40058188743 がセルフホストで完走したため、その結果も確認した。

8回の有限再試行を終えても、公開確認できたのは62記事中27記事。残る35記事はHTTP 404で公開未確認だった。メタデータ検証は成功し、公開URL確認で失敗している。JSONレポートは同じ実行のArtifact 12546009380 に保存された。

前日の記録 2026-10-23-zenn-rate-limit-confirmed.md では、Zennのデプロイ通知から投稿上限を確認済み。ただし今回の公開URL検査だけで、新しいデプロイ通知の内容や正確な制限解除時刻までは確認できない。

ハマったこと・学んだこと

  • セルフホストRunnerへの移行が適用されたことと、外部サービスの投稿制限が解消されたことは別問題。同じ実行環境で1件成功後に3件が429となった事実から、移行だけで全面解決したとは判断しない。IPだけの制限か、アカウントなど他の条件かも、この観測だけでは断定できない。
  • Sync Qiitaステップは個別の失敗を収集して後続処理へ進む設計なので、そのステップが緑でも全記事成功とは限らない。最後の集約判定、.qiita/items.json、.buffer/posted.jsonを突き合わせる。
  • 1件が成功して3件が失敗しても、成功済みIDの保存を行い、成功分を巻き戻さない。X投稿もQiita同期に成功した記事だけを対象にした。
  • Zenn検証は投稿そのものではなく、公開ページの読み取り確認。キャンセル、HTTP 404、通信エラーを公開成功とは扱わない。

公開処理の識別・再実行・入力検証を修正

bugfix未リリース

公開処理の識別・再実行・入力検証を修正

リリース状況

未リリース。この記録作成時点ではローカル回帰テストと全コンテンツ検証を実施した段階。PR最新コミットのCI成功確認後にmainへ取り込む。変更後の本番Qiita APIによる投稿・削除と、各公開先への本文反映は未確認であり、記事公開の復旧完了を意味しない。

何を変えたか

  • Issue #16: Qiita DELETE成功後に対応表の保存に失敗しても、次回の404を既削除として対応表を復旧できるようにした。401・403・429・5xxは成功扱いしない。
  • Issue #23: タイトルだけの一致による記事ID復旧を廃止。新規投稿・更新時に本文へリポジトリと記事パスのHTMLコメントを付け、一意に一致する記事のみ復旧する。同名の未識別記事がある場合、識別情報が重複する場合、一覧の走査を完了できない場合は更新・新規投稿を停止する。
  • Issue #24: 日次ニュースの日付をdatetime.date.fromisoformatで確認し、存在しない日や非うるう年の2月29日を拒否する。ファイル名とfront matter、項目内の日付の一致検査も維持する。
  • Issue #29: 通常記事・ニュースの重複キーを拒否。引用キーやUnicodeエスケープされた同じキーも検出する。プロダクト更新の既存チェックを維持し、共通の重複検査も適用する。
  • Issue #36: Git差分をname-statusのNUL区切りで解析し、renameの旧・新両パスを取得する。比較元で公開済み、またはQiita / Buffer記録のある記事のrenameは外部投稿より前に拒否し、PRでも同じ検証を行う。公開済み記事の削除と新規ファイル追加の同時変更も拒否する。未公開・未投稿の下書きrenameは許容する。

なぜ変えたか

5件とも現行コードに問題の原因が残っていた。タイトル一致は記事の同一性を保証せず、削除済みIDへの再DELETEは永続化失敗からの復旧を妨げる。入力形式が合っていても、重複キーや存在しない日付はconsumerで異なる結果になる。またname-onlyの差分ではrename元のパスを失い、孤児の対応表や誤投稿を生む。

ユーザーにとって何が良くなったか

別記事の誤上書き、削除の復旧不能、不正なニュース入力、記事パスと投稿記録の不整合を公開前に防ぐ。既存のQiita / Buffer IDや記事slugは変更していない。

技術的な内容

Python標準ライブラリを使用。外部投稿を模擬するテストと一時Gitリポジトリの実際のrenameで確認した。回帰テストは修正前に失敗を確認し、修正後に全テストと記事・ニュース・更新記録の検証を実行した。PRで履歴比較するためcheckoutのfetch-depthを0にした。

判断したこと・トレードオフ

識別情報のない既存Qiita記事は、対応表を失った場合に手動照合が必要になる。タイトルだけで自動更新する便利さより、別記事を壊さないことを優先した。既存マッピングがある通常更新は、そのIDを保持して更新する。Gitのrename検出は類似度に依存し、大幅な書き換えで削除・追加となる。そのため公開済み記事の削除と新規ファイル追加が同時にある場合も拒否する。意図した独立の操作は別々のPRに分ける必要がある。単独の削除や通常更新は維持する。

ハマったこと・学んだこと

  • APIの成功と状態保存の成功は別。再実行時の404を想定しない削除は、途中成功から復旧できない。
  • 一意なタイトルは永続IDではない。識別用の情報と、利用者が編集するタイトルを分ける。
  • 差分のパス一覧だけではrenameと削除・追加を区別できない。検証に必要なGitステータスを残す。
  • Python側で受理した入力がサイト側でも受理されるとは限らない。今回の手書きパーサーはYAML全体の実装ではなく、トップレベルのキー重複に範囲を限定している。

日次ニュースの一次情報再監査と公開工程の独立確認

reliability未リリース

日次ニュースの一次情報再監査と公開工程の独立確認

リリース状況

サイトでの掲載確認まで含めた公開復旧は未リリース・未完了。2026年10月23日分の原稿50件は、PR検証成功後にmainへ反映済み。公開処理の検証とBufferへの告知1件の作成も成功した。chicchi.workの本番反映、およびX本体での配信完了はこの記録時点で未確認であり、GitHubのマージやBuffer受付を代替の成功証拠とはしない。

何を変えたか

対象日を実行開始時のAsia/Tokyoの昨日に固定し、既存32件を全件見直した。試用キャンペーンと資格マップ掲載の2件を除外し、20件を新規採用して50件に増補した。採否を記録した候補は66件で、検索ヒット数や閲覧URL総数ではない。

技術的な内容

閲覧環境で公式本文を取得できない場合に、同一リポジトリの一時的な調査用PRから、セルフホストRunnerで公開ページを読み取り専用取得した。HTTPS・公開IP限定、リダイレクト先の確認、通信タイムアウト、応答サイズ上限、同時取得数制限を設け、秘密情報を渡さず、checkoutの認証情報も永続化しなかった。調査用ワークフローは本番ブランチにマージしない。

HTTPステータス、取得日時、本文ハッシュを記録したうえで、本文を実際に読み、日付・主張・数値・提供時期・地域や対象・重複を照合した。初稿後に全50件を改めて取得して再読し、10項目を訂正または明確化した。再監査段階の削除は0件。

ハマったこと・判断したこと

HTTP 200でもJavaScriptで本文を別途読み込むページでは、本文を取得できていない場合がある。同じ発表元の公式XML配信データまで確認し、HTMLの取得成功だけで確認済みとしなかった。

公式ページの発表日、製品の発売日、過去の発生日時、追加報告の日付は別々の情報である。海外の公開履歴はタイムゾーンを確認して日本時間へ換算した。モデルの発表と重みデータの新規公開も区別した。

形式検証ではこれらの事実関係を判定できない。scripts/validate_news.py による必須メタデータ・連番・件数・対象日の検査と、一次情報本文による事実確認を別の工程として扱った。

公開工程の到達点

  • PR最新コミットでTest publishing scripts成功後にマージ。
  • ニュース原稿コミット: 8bf3e7aeceaf475b2820e54e5206935acc74bb6d。
  • マージ後のPublish articles and newsは成功。ニュース以外の過去記事の再投稿は行っていない。
  • Bufferの投稿台帳に対象記事のpostedが1件あることを確認。これはBuffer受付であってX配信完了を保証しない。
  • 記事検証、原稿保存、X告知、サイトデプロイを分離し、後続工程が未完了でも原稿を巻き戻さない。

防御的コピーの記事で分かった、公式仕様の検証と公開完了判定の分離

reliability未リリース

防御的コピーの記事で分かった、公式仕様の検証と公開完了判定の分離

リリース状況

GitHub mainへの記事追加は完了、Qiita・Zennへの外部公開は未完了。 したがって全公開先への配信については未リリースとして記録する。2026-10-24の通常Tips記事「Java recordとList.copyOf:変更されないAPI DTOの作り方」はPR #64からmainにマージした。

何を変えたか

  • 既存記事一覧を確認し、未使用のJava record/防御的コピーを新規テーマとして選択。
  • OracleのJava SE 27公式APIでRecordの浅い不変性、List.copyOfのnull拒否とコピー特性、Collections.unmodifiableListのビュー特性を照合。
  • Javaの完全実行例をOpenJDK 21でjavac --release 17指定でコンパイル・実行した。
  • 新記事のタイトル長(UTF-16換算41文字)、slug、front matterを事前確認し、PRのTest publishing scripts成功後にマージした。
  • 先に既存未公開記事のQiita同期を再試行し、その後の新記事のmain反映で公開Workflowを起動した。

なぜ変えたか

記事のGitHub反映だけを公開成功と誤認しないこと、公式APIの根拠が明確な再現可能な実務Tipsを提供することが目的。過去にタイトル長違反でZennのデプロイが止まったため、全件の事前CIを通すPR運用を守った。

ユーザーにとって何が良くなったか

Javaのrecordの浅い不変性とListのコピー/ビューの違いを、実行可能なコードと出力で理解できる。ただし新記事のQiita/Zenn閲覧はまだ保証できないため、公開状態はサービス別に示す。

技術的な内容と結果

  • PR最新コミットe945ddc40946f8518643ba0c2f927b549531e011のCI実行40080931240は成功。
  • PR #64はmainにマージ済み。新記事パスはarticles/2026-10-24-java-record-list-copyof-defensive.md。
  • Qiita同期実行40080968825はセルフホストRunnerで実行。従前の未公開3記事、当日のCodex記事と新Tips記事の計5記事を回収対象としたが、すべてQiita HTTP 429(投稿数制限)で失敗。
  • 新Tips記事のQiita IDは.qiita/items.jsonにまだなく、Buffer投稿記録もない。Qiita連携成功記事のみBuffer登録する既存方針を維持。
  • Zenn検証40080968776は64公開対象記事のうち28記事だけHTTP 200等の一致を確認。残り36記事はHTTP 404で未確認。Zennの投稿制限については以前のデプロイ通知の記録があるが、今回の検証結果だけで最新デプロイ通知の内訳は断定しない。

Before / After

Before

10月21〜23日の残存記事や10月24日のCodex記事には未保存のQiita IDがあり、当日の通常Tips記事はまだ存在しなかった。

After

新しいJava Tips本文はmainに存在する。外部投稿制限のため、Qiita・Buffer/X・Zennの公開完了は別工程として未完了のまま残っている。成功していない工程を成功扱いしない。

判断したこと・トレードオフ

  • mainへの直接コミットではなく、過去のZennタイトル長障害を踏まえたPRで事前CI成功を確認してからマージした。
  • 新しいslugに変えて投稿制限を回避する、別アカウントに変更するなどの方法は取らない。
  • GitHubへの本文反映が完了しても、外部APIの429を理由に定期投稿設定は無効化しない。
  • 記事作成時に別バージョンでの挙動を推測せず、Java 17互換のコンパイルオプションで検証した範囲と、Oracle API仕様の範囲を分けた。

ハマったこと・学んだこと

  • recordは不変オブジェクトのように見えるが、要素の内部状態までは保護しない。List.copyOfも要素オブジェクトの深いコピーではない。
  • 外部記事配信は一部成功する可能性があるため、ジョブの成功/失敗表示だけではなく、Qiita ID・Bufferの投稿記録・ZennのURL検証を照合する必要がある。
  • ZennのURL 404とQiitaのHTTP 429は別サービスの別エラーであり、GitHub ActionsのRunnerの正常稼働とは分けて扱う。

Codex Day 19の技術調査記事作成と公開状況

docs未リリース

Codex Day 19の技術調査記事作成と公開状況

リリース状況

外部公開は未リリース。 PR #56は最新コミットで公開前テスト成功後にmainへマージ済み(コミット23aaba5e728d3a8e5f1f084b4b47a5372fbb98fc)。記事原稿はGitHubで閲覧可能。Qiitaは新規投稿制限のHTTP 429で投稿失敗し、X告知もQiita同期失敗のためスキップされた。Zennは公開確認WorkflowでHTTP 404となり、Day 19記事は未掲載。chicchi.workの公開は未確認であり、外部サイトにすべて公開済みとはしない。

何を変えたか

  • 28日間アップデートDay 19を一次情報ベースで解説する記事を作成。
  • Tibo氏の発表とOpenAI公式CLI 0.164.0リリースノートの情報を分離して記載。
  • 推論切替時のキャッシュ維持、Dotsの通話接続高速化、プリセット、MCP認証、長いセッションの復旧、macOS/Windowsの安全性改善などを整理。
  • Mac向けのCLI更新コマンドにそれぞれの引数の意味と利用時の注意点を追加。
  • Zennのタイトル上限70 UTF-16コード単位を超えないタイトルとslugを採用した。

なぜ変えたか

短い告知だけではキャッシュ無効化を防ぐ改善と一般的なAPIキャッシュ仕様を混同しやすく、CLIの新機能・バグ修正・セキュリティ改善を把握しきれない。未公表の性能値を事実と混同せず、日々の開発に役立つ技術解説として残す必要があった。

また、記事の公開運用には過去にZennのメタデータ不備・QiitaのHTTP 429で公開が止まった経緯があるため、PR検証と公開先ごとの実在確認を必須にした。

ユーザーにとって何が良くなったか

  • /presets、/model、/permissionsの使い分けとAGENTS.mdとの差を理解できる。
  • キャッシュの改善対象を正しく把握でき、使用量減少率を誤解せずに済む。
  • macOSや企業向けWindows環境での更新時に、認証・権限・画像描画・履歴復元の確認項目を把握できる。
  • 開発作業で再利用可能なバグ調査・レビュー用プロンプト例を参照できる。

技術的な内容

  • OpenAI公式リリース: rust-v0.164.0、2026-10-23 04:48 UTC。
  • CLIに10個のプリセット、インラインモデル・権限設定、Kitty/Sixel画像プレビュー、企業向けMCPサインイン、リモート実行での資格情報マスキング、Amazon Bedrockのウィザード改善が含まれる。
  • compaction失敗時の部分要約、再開時の保存済みスレッド設定、Windows Negotiate/NTLMプロキシ、macOSメタデータパス・symlinkチェック等の修正を公式ノートに照合した。
  • Dotsの接続秒数、キャッシュ削減率、速度や利用枠の倍増など、公式が数値を示していない点は断定しない。

Before / After

Before

Day 19の短い公式投稿だけでは実際のCLIリリース内容や制約、環境ごとの影響が分かりにくかった。

After

公式リリースノートと関連文書を参照した技術記事で仕様、未確認範囲、導入例をまとめた。記事の公開結果は公開先ごとに追跡する。

判断したこと・トレードオフ

  • CLIを実行していないため実機レビューのように書かず、公式仕様の解説に限定する。
  • APIの12時間キャッシュ(Day 16)とCodex/Workの推論切替(Day 19)の違いを明示する。
  • コマンドは公式が確認できたnpm install -g @openai/[email protected]、codex --version、codexなどに限定。画面・モデルによって変わる引数を推測しない。
  • マスクの対象外の経路や企業向け認可の制約を併記して、過剰な安全保証に見せない。
  • mainへの直接pushではなく、PRのテスト成功後にマージする。

ハマったこと・学んだこと

  • リリースの配布時刻とDay 19の告知時刻は別。日付のタイムゾーンを混同しない。
  • 「利用量2倍」と「処理速度2倍」は異なる。製品のデータが示す対象を正確に特定する。
  • Kitty/Sixel対応は端末依存。macOSならすべて画像プレビュー対応という誤解を避ける。
  • 資格情報のマスキング・macOSのパス保護強化は新機能だけでなくセキュリティ面の変更でもある。
  • コンパクション修正によるセッション復元は無制限コンテキストの提供ではない。

Zennの入力検証とディレクトリ修正は反映済み、投稿上限分は保留

releaseリリース済み

Zennの入力検証とディレクトリ修正は反映済み、投稿上限分は保留

リリース状況

検証・構成修正はリリース済み。Zennの全記事公開は未完了。

PR #42でタイトル長検証と全件の公開確認、PR #43で本文を維持した未同期記事の再保存、PR #46でbooks/.gitkeepと原因・対応記録をmainへ反映した。PR #46の最新headでは自動テストが成功してからマージした。構成修正のマージコミットは23677413dcb02c288bce104d2367f963d6d6c27e。

通知に列挙された31記事は、Zenn側が投稿数上限による未デプロイを明示している。この31件を公開済みとして扱わない。後から追加された記事の件数と、当初の通知に列挙された件数は区別する。

何を変えたか

  • 過去59ファイルの初回監査で見つかったタイトル長違反2件を修正。
  • 70文字上限を公式CLIと同じUTF-16単位で検証し、公開処理と全記事検査へ適用。
  • 全公開記事のURLと現在のタイトルを独立して検証する仕組みを追加。
  • 空のbooksディレクトリをGitに保持する.gitkeepを追加。新しい本は作成しない。
  • 後続の31件はコード不備ではなく投稿上限と判明したため、無意味な再同期の連打を止めた。

なぜ変えたか

過去の長いタイトルでデプロイが停止した問題と、その修正後に残ったサービス側の投稿数制限が重なっていた。booksディレクトリ不在の通知も別の構成問題であり、同一原因として扱うと誤った復旧操作になる。

ユーザーにとって何が良くなったか

タイトル長違反の再発を公開前に検出でき、公開後も未反映を見落としにくくなった。本文・slug・既存投稿IDを維持したまま修正し、未公開を公開済みと誤報しない。

技術的な内容

Zenn公式FAQは記事の直近24時間以内の投稿数(投稿予約中を含む)を判定対象と説明し、上限算出ロジックは非公開としている。一定時間が経過すると再度投稿できるが、対象アカウントの正確な解除時刻や残り件数を外部から確定できない。移行に該当する場合は、公式問い合わせによる一時的な上限緩和の申請経路がある。

books/.gitkeep追加は本用フォルダー不在への対処であり、記事投稿枠を増やしたり、投稿上限を解除したりする変更ではない。

判断したこと・トレードオフ

  • mainへの記事変更はPRの最新テスト成功後にマージする運用をAGENTS.mdへ明記。ブランチ保護の管理設定自体は変更していない。
  • サービス側の上限を無視する仕組み、別アカウント・別slugへのコピー、公開確認の成功条件緩和は行わない。
  • Zennの制限を他の公開先に波及させない。chicchi.workは独立して59記事と一覧の全60ページ公開を確認済み。

ハマったこと・学んだこと

入力検証の修正成功、main反映、外部の公開処理完了は別の状態である。通知の具体的な理由を公開ページの確認結果と照合することで、入力エラーと投稿枠不足を区別できる。

過去記事のタイトル超過で止まったZenn公開の復旧と再発防止

bugfix未リリース

過去記事のタイトル超過で止まったZenn公開の復旧と再発防止

リリース状況

未リリース。PR #42で修正・検証中。mainへのマージと公開先での反映確認は別工程として扱い、確認できるまでZenn公開済みとはしない。

何を変えたか

  • 既存59記事を全件検証し、Zennの70文字上限を超えた2記事を特定。
  • 2026-10-06のCodex Day 2記事を76文字から41文字、2026-10-08のDay 3記事を77文字から44文字へ短縮。
  • 本文、ファイル名、既存のQiita記事ID、Buffer投稿状態は維持する。
  • 共通メタデータ検証に70文字制限を追加し、公開記事だけでなく下書き・過去記事も検査する。
  • 日本語・ASCII・絵文字・境界値・複数エラー・API呼出し前拒否を確認する11件の回帰テストを追加。
  • Zenn公開ページのHTTP状態・canonical URL・現在のタイトルを照合する読み取り専用チェックと、その9件のテストを追加。
  • mainの記事更新後に全公開記事を確認する独立した Verify Zenn publication ワークフローを追加。
  • AGENTS.mdにPR検証後のマージ、公開先ごとの成功確認、未確認を成功扱いしない運用を明記。

なぜ変えたか

Zennのデプロイ履歴は、古い記事の保存が「Titleには最大70文字まで使用できます」で失敗し、更新されたファイルがないと報告していた。Qiita同期の成功だけではこの障害を検知できず、新しい記事もZennに掲載されない状態になっていた。

ユーザーにとって何が良くなったか

過去記事の不備をまとめて解消し、今後は新しい記事だけを確認して古い違反を見落とすことを防ぐ。記事URLを変えないため既存リンクを維持できる。Zenn側の未反映も独立したチェック失敗として確認できる。

技術的な内容

Zenn公式 zenn-model/src/utils.ts の validateTitleLength は title.length <= 70 を使う。Pythonの len(str) はUnicodeコードポイント数、JavaScriptのString.lengthはUTF-16コード単位数なので、単純な移植では一部の絵文字で差が生じる。

len(text.encode("utf-16-le", errors="surrogatepass")) // 2 により公式CLIと同じ単位で検証した。見た目の書記素数やUTF-8バイト数とは異なる。

既存の共通検証関数はPR、公開ワークフロー、Qiita API呼出し前から使用されているため、検証条件を一箇所に追加した。既存の記事全件テストにより、修正前は2件の違反で失敗することを確認した。

公開確認では、全公開記事を対象にHTTP 200・リダイレクト先・canonical・h1タイトルを照合する。下書き・未来の予約記事は対象外。同期遅延は未確認ページのみ最大8回、20秒間隔で再試行し、ページ取得に失敗しても公開済みと誤判定しない。JSONの結果はActionsの成果物として14日保持する。

Before / After

Before

タイトルの空文字・型は検証していたが長さを検証していなかった。Qiita公開成功とZennへの反映が独立であることを運用で十分に確認できていなかった。

After

全記事のタイトル制限を公開前のPRテストで検知し、公開後のZennページも全件確認する。公開先別の結果を記録する。

判断したこと・トレードオフ

  • タイトルだけを意味を保って短縮し、URL変更や本文の削除を行わない。
  • 記事全件を機械的に切り捨てる処理は実装しない。違反はエラーにして人間または執筆エージェントが修正する。
  • ZennのGitHub同期はmainへのpush時点で開始するため、push後のCIだけで事前防止できるとは説明しない。記事変更はPR検証成功後にマージする。
  • ブランチ保護の管理設定は変更していない。mainへの直接pushを強制的に禁止する管理設定とは別である。
  • 公開確認はページの存在とタイトル反映を検査するが、本文全体の同一性や全種類のZenn制約を保証するものではない。
  • HTTP 403や通信障害も公開未確認として失敗する。障害と記事未反映を区別できるログを残す。

ハマったこと・学んだこと

  • 過去記事の1件の不備が、後続の正常な記事の公開まで止める場合がある。
  • 複数公開先を持つ場合、片方のAPI成功はもう片方の公開成功ではない。
  • 異なる言語間で文字数制限を移植するときは、文字列の数え方を一致させる必要がある。
  • 公開済み記事のタイトル修正は、投稿IDを維持してPATCH更新し、Xの再告知を発生させないことをテストで確認する。

Zenn復旧後の未公開31記事は投稿数上限と確認

reliability未リリース

Zenn復旧後の未公開31記事は投稿数上限と確認

リリース状況

タイトル検証と公開確認機能はPR #42でリリース済み。PR #43の31件再同期もmainへ反映済み。ただし、Zenn公開対象58記事のうち27件だけ確認でき、残る31件は未公開である。

この記録とbooks/.gitkeepの追加は未リリース。Zennの全記事公開が完了したという記録ではない。

何を変えたか

  • デプロイ通知で、残る31記事が「投稿数の上限に達したためデプロイされませんでした」と明示されたことを確認。
  • タイトル長違反、投稿数上限、booksディレクトリ不在を別々の状態として整理。
  • 空のbooksディレクトリをGitで保持するbooks/.gitkeepを追加。本の原稿や公開設定は作らない。
  • 制限中に再同期を繰り返す復旧試行を止める。新しいslugや別アカウントなどで制限を回避しない。

なぜ変えたか

タイトル制限の修正後も未公開記事が残ったため、全件URL照合だけで原因を推測せず、Zennのデプロイ通知と公式FAQで確認した。残る31件はタイトルエラーではなくサービス側の投稿上限である。

ユーザーにとって何が良くなったか

コードの修正完了とサービスへの全記事反映完了を区別し、まだ公開されていない記事を公開済みと誤報しない。別公開先のchicchi.workは独立して公開できるため、Zenn制限の解除を待たせない。

技術的な内容

Zenn公式FAQでは記事の上限判定は直近24時間以内の投稿数(予約中を含む)に基づく。具体的な上限は複数要素から算出され、ロジックは非公開。日付が変わった瞬間に一律リセットされる仕様とは説明されていない。一定時間経過で再度投稿可能となるが、対象アカウントの正確な解放時刻や残り枠は外部から確定できない。

他ブログからの移行の場合は公式問い合わせフォームで一時緩和を申請できると案内されている。申請や承認なしに緩和されたとは扱わない。

判断したこと・トレードオフ

  • 31記事のpublished、本文、slug、投稿IDを維持し、他公開先を巻き込んで下書きへ戻さない。
  • books/.gitkeepはディレクトリ不在の対処であり、記事投稿枠を増やすものではない。
  • 公開確認workflowはHTTP 404を成功に変更しない。上限に起因する未公開が残る間は失敗を維持する。
  • 上限が残ったまま無駄な再デプロイを連打しない。解除後の通常の再デプロイ、または公式への移行相談で対応する。

ハマったこと・学んだこと

入力データの不正を直した後にも、外部サービス固有の投稿枠が次の障害として現れる場合がある。HTTP 404は未公開を示すが、原因が入力エラーなのか投稿上限なのかはデプロイ通知との照合が必要。

Zennのタイトル不備と投稿数上限を区別し、本用ディレクトリを整備する

reliabilityリリース済み

Zennのタイトル不備と投稿数上限を区別し、本用ディレクトリを整備する

リリース状況

構成変更はリリース済み。 PR #47がmainへマージされ、books/.gitkeepの追加を反映した。先行するタイトル制限修正・全件検証はPR #42で反映済み。

Zennへの全記事公開は未完了。 PR #43の再同期後にも、通知に記載された31記事が投稿数上限で公開されていない。books追加後の管理画面で警告が消えたかは未確認。フォルダー追加と投稿数制限の解除は別であり、このstatusは全記事の公開成功を意味しない。

何を変えたか

  • リポジトリ直下にbooks/.gitkeepを追加し、本用ディレクトリ不足への構成修正を行った。本の本文や有料コンテンツは追加していない。
  • 追加のデプロイ通知から、残る31記事の未公開理由が「投稿数の上限」であると確認し記録した。
  • 再試行で上限を解除できるという前提を撤回し、短時間の再保存や連続再デプロイを復旧手段として使わない。
  • chicchi.workの更新停止を別のビルド起動問題として扱う。

なぜ変えたか

初期のタイトル上限エラーを修正した後にも公開されない記事が残った。公開ページの404だけでは原因を特定できなかったが、Zenn側のデプロイ通知が投稿数上限を明示していた。入力不備、外部サービスの制限、本用フォルダーの警告、別サイトのビルド未実行を混同しないため。

ユーザーにとって何が良くなったか

復旧できた修正と外部制限による公開待ちを区別し、QiitaやGitHubの成功だけでZennまで公開済みと報告しない。不要な再試行や新規slugによる重複記事を避ける。

技術的な内容

Zennの2026-10-23確認時点の公式FAQでは、記事は直近24時間の投稿数(投稿予約中を含む)を基準に判定する。具体的な上限値と判定ロジックは非公開である。時刻が0時を過ぎれば必ず解除される、1日何件まで投稿できる、といった固定値を設定しない。

投稿枠が回復した後に、未公開の既存slugを再同期して公開ページで確認する。31件すべてが次の1回で公開できる保証はない。移行目的には公式問い合わせによる一時的な上限緩和申請が案内されているが、申請や承認は本変更で行っていない。

books/.gitkeepはGitで空の本用ディレクトリを保持するためのファイルであり、投稿数上限を解除するものではない。

判断したこと・トレードオフ

上限回避のためにアカウント・slug・コンテンツ種類を変える方法や、予約投稿で制限を避ける方法は採用しない。公開済み記事の非公開化・削除も行わない。現在のpublished設定、本文、URL、Qiita/Buffer投稿状態は維持する。

公開確認用HTTP GETの再試行と、記事を再投稿・再同期する操作は異なる。ページを再確認しても投稿枠は回復しない。今後の公開報告では「形式検証成功」「公開処理実行」「公開ページ確認」「レート制限による公開待ち」を分ける。

ハマったこと・学んだこと

1つの不具合を修正しても、蓄積された未公開記事をまとめて同期することで別の上限へ到達する場合がある。エラー画面だけでなく、最新デプロイの警告と公開ページの全件確認を組み合わせる必要がある。

タイトル制限の修正後にも残ったZenn未公開記事の再同期

reliability未リリース

タイトル制限の修正後にも残ったZenn未公開記事の再同期

リリース状況

未リリース。この追加復旧は作業ブランチで準備した段階。先行するPR #42のタイトル修正と検証機能はmainにマージ済みで、111件のテストと全59記事のメタデータ検証、Qiita同期は成功した。ただし、それをもってZennへの全記事公開成功とは扱わない。

何を変えたか

公開後の全件照合でHTTP 404が確認された31記事について、front matterの終了区切り直前へ空行を1行追加した。記事の内容を変えずにGit上の変更として再保存し、Zennの同期対象へ戻すための復旧操作である。

本文のバイト列、解析後の全メタデータ、タイトル、slug、published、既存のQiita/Buffer投稿IDは変更していない。新しい記事ファイルや別URLへのコピーは作らない。

なぜ変えたか

PR #42を2026-10-23 14:51(日本時間)にmainへ反映した後、14:54時点の公開確認レポートでは、59ファイルのうち公開対象58件、確認成功27件、HTTP 404が31件だった。下書き1件は公開対象に含めない。

Zenn公式は変更されたMarkdownを同期すると説明している。未公開の理由を差分処理だけと断定することはできないが、残った31記事を意味を変えずに再保存し、再同期後のページを確認する。

ユーザーにとって何が良くなったか

過去の記事も実際の公開ページで確認し、失敗したままの状態を成功と報告しない。既存の本文とURLを維持したまま復旧を試みる。

技術的な内容

公開チェックのJSONレポートから、失敗理由がHTTP 404である31件のパスを固定して処理した。すべてarticles配下の通常ファイルであり、共有バリデーターを通過し、publishedがtrueであることを確認した。

元のUTF-8バイト列へfront matter内の空行を追加し、変更前後でparse_frontmatterの結果が完全一致すること、および終了区切り以降の本文バイト列が完全一致することを検証した。全件検証・回帰テストが成功した場合だけ隔離した作業ブランチへコミットした。一時的な準備ワークフローは処理完了後に削除し、mainへ残さない。

判断したこと・トレードオフ

  • 正常に公開確認できた27件と下書き1件は再保存しない。
  • 本文、タイトル、公開日時の設定を変更して差分を作る方法は採用しない。
  • HTTP 404を無視したり、公開確認をスキップして成功にする変更は行わない。
  • 再同期後もZenn側の反映を確認する。未反映が残る場合、完了とは扱わない。

ハマったこと・学んだこと

複数公開先の同期では、原因となる入力エラーの修正と、失敗中に蓄積した未公開記事の復旧を分けて確認する必要がある。外部ページを全件照合する検査によって、GitHub ActionsやQiitaの成功だけでは見えない未反映を検出できた。

Muse記事冒頭の紹介コード案内を改善

docs未リリース

Muse記事冒頭の紹介コード案内を改善

リリース状況

未リリース。PRマージおよび自サイトの再デプロイ・公開確認をもってリリース済みとする。

変更

Muse比較記事のフロントマター直後に、紹介コードYM056K、公式参加URL、登録から48時間以内の設定画面入力手順、条件付き10億トークン特典を案内する引用ブロックを追加。

設計判断

記事本文の比較解説より前に申し込み手順を置き、登録後の入力忘れを防ぐ。紹介者にも特典が付与される事実を同じブロック内で明示し、特典の適用を保証しない。記事のslugと公開設定は変更しない。

技術的な知見

Markdownのfront matterを保持したまま、その直後へ案内を置くと、自サイト・Qiita・ZennなどのMarkdown処理で記事の最初に表示できる。既存の見出し・コード例・比較表は変更しない。

確認事項

記事本体を差分確認し、GitHub Actionsの検証が成功すること。外部公開完了は別途確認する。

Muse招待キャンペーン解説記事の追加と利益相反の明示

docsリリース済み

Muse招待キャンペーン解説記事の追加と利益相反の明示

リリース状況

リリース済み(2026-10-23)。PR #40がmainへマージされ、記事の公開用GitHub Actionsがsuccessで完了。Qiita同期処理は成功し、公開記事ID b5ad353c25ac21345ae5 が .qiita/items.json に記録された。Zennとchicchi.workでの閲覧可否はこの時点では未確認であり、chicchi.workは別リポジトリの再ビルドが必要な構成。

何を変えたか

  • 招待キャンペーンの条件を調べ、読者が最初に操作できる位置にコードと登録先を配置した記事を作成。
  • 登録後48時間以内の引き換え、紹介者と参加者の双方への特典、利用可能地域・紹介上限の注意点を追記。
  • 記事の冒頭で、紹介コードを利用すると記事側にもトークンが付与されることを開示。
  • Museの専用VM・Sentinel・アクセス権限設計を公式資料に基づいて解説。

なぜ変えたか

紹介コード利用による報酬獲得に際し、単なるコードの羅列でなく、読者が条件を理解して適切に利用できる説明を求められたため。

ユーザーにとって何が良くなったか

登録と引き換えの違いを明確化し、48時間の期限切れや紹介特典の利益相反について誤認しにくくした。

技術的な内容

  • 記事をarticles/*.mdのZenn/Qiita共通front matterで管理(published: true)。
  • GitHub ActionsによるQiita連携、Zenn GitHub連携、Web側取り込み対象として扱う。
  • 報酬条件を「当事者が示す招待文面」と「Meta関係者の公開告知」の範囲に分け、個別画面で食い違う招待枠は断定しない。
  • CTA(読者に次の操作を促す導線)を記事冒頭・末尾に置きながら、受益者を明示する。

Before / After

Before

該当するMuseキャンペーンの解説記事がなく、招待コードの使い方も未掲載だった。

After

コード入力手順、キャンペーン上限の情報差、地域制限、Museの安全設計を1記事に整理した。

判断したこと・トレードオフ

  • コードをURLに埋め込める証拠がないため、公式/joinへのリンクと、コードの手動入力を分けて記載する。
  • 一部の紹介記事には30人上限、Meta関係者の告知には20人上限があるため、公式告知を基準とし、30人は画面次第の参考情報として記載する。
  • 期間・地域・トークンの現金価値について、未確認情報を断定しない。
  • 投稿記事に私的なアカウント情報や認証情報を含めない。公開を意図した招待コードのみ扱う。

ハマったこと・学んだこと

  • 招待特典の「参加後48時間」と「コードを持っていること」は別条件。URLを踏むだけでは付与条件を満たさない。
  • キャンペーンの紹介人数上限は情報源によって20人・30人と食い違う。単一の推定値を断定すると誤案内になる。
  • 地域未提供のサービスでは、特典コードとサービス利用資格を区別する必要がある。

Muse記事を比較中心へ変更:紹介訴求の整理と一次情報の照合

docsリリース済み

Muse記事を比較中心へ変更:紹介訴求の整理と一次情報の照合

リリース状況

リリース済み(2026-10-23)。PR #41をmainへマージし、GitHub Actions「Publish articles and news」(run #39901899810)が成功。Qiita同期先の記事IDが既存の b5ad353c25ac21345ae5 のままであることを確認。Zennおよびchicchi.workの実際のページ表示は未確認。

何を変えたか

  • 記事タイトルを「Museとは?ChatGPTやGrok Botとの違いと無料で10億トークンを受け取る方法」に変更。
  • コード入力手順と特典説明が記事の大半を占めていた構成を改め、Museの機能、ChatGPT(Chat/Work)、Grok Botの比較を中心に再構成。
  • 読者向けの紹介コード案内を後半の一箇所だけにまとめ、記事冒頭やまとめでの重複表示を解消。
  • Grok(会話AI)とGrok Bot(持続的なクラウドPCを持つ役割別Bot)が別製品である点を明記。
  • ChatGPT Workもブラウザ・連携サービスを使って複数ステップの仕事を実行し、アプリを閉じた後も作業できるという公式仕様を反映。
  • Metaの2026-09-24 Connect発表(Mac版、外部アプリとの接続拡大)を追加。
  • Meta公式の上限20人を基準とし、根拠が不十分な30人上限の試算を記事から削除。
  • Museが日本で正式に提供される時期は未定という公式の注意書きを維持。

なぜ変えたか

当初の記事が紹介特典を過度に押し出していたため、読者がサービスの特徴・他社製品との違いを理解できる構成へ変更。比較の公平性と一次情報への整合性を優先する。

ユーザーにとって何が良くなったか

MuseとChatGPT Work、Grok Botを用途・作業環境・背景実行・プラン・権限で比較できる。興味を持った読者だけが自然に紹介特典へ進む導線になった。

技術的な内容

記事の正本は既存の articles/2026-10-23-meta-muse-referral-1b-tokens.md として維持し、Zenn/Qiitaの既存記事URL・投稿IDを保持する。Markdown front matterのtitleのみ更新し、slugは変更しない。

比較では、Museの専用VMとSentinel、ChatGPT Workのクラウドブラウザ、Grok Botの同一ユーザー内Bot間のPC共有というアーキテクチャ差を説明する。製品の「公式に提供する機能」と「性能評価・推測」は混ぜない。

Before / After

Before

特典コードが冒頭・表・FAQ・まとめに頻出し、比較対象はほぼChatGPTの簡単な説明だけ。20人/30人の根拠が弱い両論が中心となっていた。

After

サービス説明、三者比較表、具体的な用途、安全設計、提供地域を中心に据え、紹介コードは本文後半に限定した。

判断したこと・トレードオフ

  • タイトルの訴求ポイントは「Museと他サービスとの違い」を先にし、無料特典は最後に添える。
  • 紹介によって筆者にも利益が発生することは、コード案内のすぐそばで明示し、隠さない。
  • 旧slugを変更すると既存Zenn/Qiita記事が別記事扱いになりかねないため維持。
  • 実測比較ではないので速度・品質順位、トークンの円換算、機能の全ユーザーへの提供を断定しない。

ハマったこと・学んだこと

  • ChatGPTの旧agent modeはWorkへ移行。比較表でChatGPTを単なる回答専用AIと書くと誤解が生じる。
  • Grok Botと普通のGrokは別物。Grok BotはBotごとのメモリと、ユーザー内で共有する1台のクラウドPCを組み合わせる。
  • Museは2026-09-24にMac版やConnectorsの拡充を発表。9月8日の初回発表だけで比較すると変更を見落とす。
  • 未確認の30人招待説を公式上限と同格に扱わない。特典説明は最小限にとどめる。

Codex Day 18記事の一次情報照合と公開前検証

docs未リリース

Codex Day 18記事の一次情報照合と公開前検証

リリース状況

未リリース(Qiita・Zennの両方で公開完了を確認できていない)。記事はPR #44で事前テスト成功後にmainへマージ済み(merge commit: 1d3a32408e86ff08787394883c0c172f341e4757)。2026-10-23の公開WorkflowでQiita APIはHTTP 429(投稿回数制限)となり新規投稿に失敗。Qiita成功が条件のX告知はスキップされた。Zennの公開検証Workflowは8回の確認後に失敗。対象59記事のうち27記事のみ公開を確認でき、残り32記事はHTTP 404。Day 18記事も404に含まれるため未公開。過去記事を含む公開未反映が継続しており原因・復旧は未確認。

何を変えたか

  • Day 18の公式X投稿とOpenAI API Changelog、Decisions API公式ブログ、AAIFの公式発表、Uber導入発表を突き合わせた記事を追加した。
  • リセットの発表と個々のアカウントへの反映完了を混同しないよう、時刻・範囲・未確認項目を明記した。
  • Decisions APIの正式提供、Open Responsesのベンダー中立的な標準化、Uberでの展開について、技術的な意味と限界を分けて整理した。

なぜ変えたか

トラッカーやSNSの短い投稿だけでは、リセットの予告、完了、一般向けAPIの仕様、企業向け展開を混同しやすかったため。過去記事で発生したZennのタイトル上限違反による全記事公開停止を避けるため、PRで検証してからマージする。

ユーザーにとって何が良くなったか

  • リセットがいつ「予定」されているかと「適用済み」かを区別できる。
  • Decisions APIを試すためのHTTPリクエスト例、基本用語、料金と制約を確認できる。
  • 標準化の告知と実際のマルチプロバイダー互換性を区別できる。

技術的な内容

Decisions APIの POST /v1/decisions は gpt-6-luna で predicate、choice、score の3種の判定を行い、GAで最大400問に対応する。入力のキャッシュ・構造化JSON・画像・会話履歴なども公式ガイドで確認した。

Open Responsesはモデル呼び出し、ツール使用、ストリーミングの共通スキーマを定義する。Linux Foundation傘下のAAIFへの参加と、すべてのモデル提供者で完全互換になることは別の話である。

Before / After

Before

Day 18の短いリセット報告のみで、内容を公式資料ごとに切り分けた技術解説がなかった。

After

正式提供されたAPI仕様と公開範囲、標準化、企業導入、適用待ちのリセットを一次情報で区別した詳細記事をPR経由で公開する。

判断したこと・トレードオフ

  • Day 18のパフォーマンス改善は具体的な測定対象・改善率が公表されていないため、推測の数値を載せない。
  • Decisions APIの「最大10倍高速」は同APIとResponses APIを比較するOpenAIの説明として記載し、Codex全体への速度向上とは扱わない。
  • Zennの公開前制限として、タイトルは70 UTF-16コード単位以内とし、mainへの直接pushは避ける。
  • ZennとQiitaの公開結果は独立して確認し、公開済みを推測で報告しない。

ハマったこと・学んだこと

  • 米国西海岸の10月にはPDT(UTC-7)が使われるが、発表文言にPST(UTC-8)が使われる場合があり、日本時間の換算には1時間の解釈差が発生する。
  • 日次アップデートは新機能と利用枠リセットが同時に発表されることがある。
  • 技術記事の誤認を避けるには、公式投稿→製品の変更履歴→公式ガイド→導入企業の一次情報という順に検証する。
  • 「GA」は一般提供であり、同じ告知中にある他の製品のリリース・性能値まで保証するものではない。

公開フローの確認結果

Day 7のQiita投稿制限が解除され、Day 8記事とまとめて公開成功

releaseリリース済み

Day 7のQiita投稿制限が解除され、Day 8記事とまとめて公開成功

リリース状況

リリース済み(2026年10月13日)。

Day 8の記事をPR #25でmainへマージ後、GitHub ActionsのPublish articles(run 38346967881)が成功。 前日にHTTP 429でQiitaへの公開が失敗していたDay 7の記事も、未公開済み記事を取り込む既存の自動再送候補選択により、同時に正常投稿された。

Qiita APIによる新規投稿成功、投稿IDの永続化、BufferへのX投稿登録まで確認。 Xへの実際の配信、Zennとchicchi.workの表示は別途確認が必要。

何を変えたか

  • 新規記事articles/2026-10-13-codex-day8-global-reset.mdを公開用mainブランチへ追加した。
  • 既存の公開Workflowが、Qiita ID未保存だったarticles/2026-10-12-codex-day7-500k-context.mdを対象として再度投稿した。
  • Day 7とDay 8のQiita IDおよびBufferへの登録結果を確認した。

なぜ変えたか

10月12日のDay 7記事はGitHubへのマージに成功していた一方、Qiita APIからHTTP 429 Too Many Requestsが返され公開できなかった。 このときの未公開状態は、過去の更新履歴2026-10-12-day7-qiita-429-publication.mdへ記録されている。

投稿制限が解除された後に、すでに公開済みのQiita記事を二重作成せず、未公開の記事のみを選択して再投稿する必要があった。

ユーザーにとって何が良くなったか

Day 7とDay 8の記事がQiitaで読める状態になり、両記事のX告知もBufferへ登録された。 前日の公開待ちを放置せず、新しい記事の投稿時に自動で回収できた。

技術的な内容

公開Workflowは.qiita/items.jsonに記事IDがないpublished: trueの記事を追加で選択する。 今回、前日未公開のDay 7と新規Day 8が同じ実行に含まれ、両方のPOSTが成功した。

確認されたURL:

.buffer/posted.jsonにも両記事のstatus: postedとBuffer投稿IDが保存された。ただし、この状態はBuffer投稿登録の成功を意味し、X側の最終配信は保証しない。

Before / After

Before

  • Day 7:Qiita HTTP 429で失敗、Qiita IDなし、Buffer投稿なし
  • Day 8:記事未作成

After

  • Day 7:Qiita新規投稿成功、Buffer投稿登録成功
  • Day 8:Qiita新規投稿成功、Buffer投稿登録成功
  • Publish articlesとTest publishing scriptsはともに成功

ハマったこと・学んだこと

  • API側の429が解消しても、公開済みかをログ・投稿ID・状態ファイルで個別に確認する。
  • CI成功とQiita公開成功は別段階。
  • 再実行時は未公開の判定に永続化されたIDを使い、同じ内容の二重投稿を防ぐ。
  • 技術更新履歴は追記型とし、過去のunreleasedの記録を改ざんせず、今回のreleased記録で進捗を更新する。

Day 7記事公開時に発生したQiita HTTP 429と配信確認

reliability未リリース

Day 7記事公開時に発生したQiita HTTP 429と配信確認

リリース状況

未リリース(Qiita・XへのDay 7記事の公開が未完了)。 2026年10月12日、Day 7記事はPR #21でmainにマージされ、独立CIは成功した。しかし公開WorkflowがQiita APIの投稿上限により失敗し、Qiitaと連動したBuffer投稿は未実施。Zenn・自サイトへの反映も未確認。運用基盤の修正コードは今回含めていない。

何を変えたか

  • articles/2026-10-12-codex-day7-500k-context.md を追加し、mainで published: true とした。
  • Qiita同期失敗の原因を公開Workflowログから確認した。
  • 投稿の成功可否を、GitHubマージ・CI・Qiita API・X投稿登録の段階に分けて記録した。

なぜ変えたか

記事のマージと公開を同一視すると、外部投稿が失敗していても「公開済み」と誤認してしまう。

GitHub Actions Publish articles(run 38192069843)では、Qiitaの新規投稿が次の理由で拒否された。

HTTP 429 Too Many Requests
type: too_many_requests
一定期間内の投稿数を制限

メタデータ検証と記事のファイル変更検出は成功し、失敗は外部APIへの新規投稿に限られる。Xへの告知処理はQiita失敗を検知し、自動でスキップした。

ユーザーにとって何が良くなったか

Day 7記事の原稿はGitHubのmainで確認できる。Qiitaで読める状態かどうかはAPI結果をもとに正確に判断でき、誤った公開成功報告を避けられる。

技術的な内容と設計判断

  • .qiita/items.json に対象記事のIDがなければ、Qiitaの新規投稿がまだ成功していない。
  • Bufferの投稿状態にも対象記事がなければ、X投稿の登録は未完了。
  • 公開Workflowはエラーがあった記事の一覧を保存し、最終ステップで非ゼロ終了するため、投稿失敗を成功扱いしない。
  • HTTP 429を受けた場合、短時間の無制限再試行は行わず、サービス側の制限に従い時間を空けて再確認する。
  • 同一タイトルの二重投稿リスクを避けるため、再実行前にQiitaの投稿ID紐付けと既存記事検索の挙動を確認する。

Before / After

Before

GitHubへのマージ成功だけで外部投稿も成功したと判断しがちだった。

After

次の段階別に確認する。

Markdown作成
  -> PR CI成功
  -> mainマージ
  -> Qiita API 2xx と投稿ID保存
  -> Buffer投稿登録
  -> X実配信(別途確認)

ハマりどころ・学び

  • Qiitaの投稿数制限は、MarkdownやActionsの不具合ではなくサービス側から返されるHTTP 429である。
  • Qiitaの投稿が落ちても「記事原稿がmainにある」「記事がQiitaに公開された」は異なる状態。
  • 一度制限されたら、理由のない即時リトライを繰り返さず、許容されるタイミングまで待つ。
  • 外部APIが拒否したケースは、認証情報やレスポンスの秘密情報を運用メモへ保存しない。

Codex Day 6記事で発生した技術テーマ検証エラーと公開までの復旧

docsリリース済み

Codex Day 6記事で発生した技術テーマ検証エラーと公開までの復旧

リリース状況

リリース済み(2026年10月11日)。 Codex Day 6の新規記事をPR #19でmainへマージし、Publish articlesワークフローの成功とQiita新規投稿APIの成功を確認した。Buffer経由のX投稿は作成登録まで成功。Zennとchicchi.workへの外部反映完了は未確認。

何を変えたか

  • 公開予定記事 articles/2026-10-11-codex-day6-global-reset.md のタイトル2行目を、OpenAI Day 6... から Codex Day 6... に変更した。
  • 記事テーマとファイル名が一致するようにし、PRの全記事検証に通した。本文と公開パスは維持した。

なぜ変えたか

PR #19のCI Test publishing scripts 内で、全記事の技術テーマ検証が失敗した。

slug と title/topics の技術テーマが一致しません:
slug=codex; title/topics=openai

記事ファイル名の技術を示す部分がcodexである一方、タイトル先頭の技術名がOpenAIであったため、メタデータのテーマ整合性検証で不一致として検出された。

ユーザーにとって何が良くなったか

  • Day 6のリセット記事をQiitaで読めるようになった。
  • GitHubのテーマ整合性チェックを回避せずに記事を公開できた。
  • 既存記事のファイル名・URL・投稿管理IDを変更せずに対処した。

技術的な内容

このリポジトリは、scripts/article_metadata.py 等の検証を通じて、記事の技術テーマとファイル名の技術テーマが矛盾しないかを確認している。

全記事の検証は独立したテストWorkflowで実行されるほか、公開Workflowの外部Qiita API書き込み前にも scripts/validate_articles.py を実行する。

今回は記事の2行目のタイトルだけを修正し、コードや検証ロジックの緩和は行わなかった。

Before / After

Before

title: "OpenAI Day 6はグローバルリセット!..."

ファイル名 2026-10-11-codex-day6-global-reset.md と技術テーマが不一致でCI失敗。

After

title: "Codex Day 6はグローバルリセット!..."

ファイル名に対応する技術テーマへ変更し、CIを通過した。

判断したこと・トレードオフ

  • 既存バリデータを変更・無効化せず、記事メタデータを修正する方式を選んだ。
  • 記事公開パスは変えず、将来のQiita / Zenn / chicchi.workのURLと紐付けを安定させた。
  • 公開処理の成功と、Xへ実際に配信されたかは別々に記録する。

ハマったこと・学んだこと

  • PRのテーマ検証はfront matterの構文だけでなく、slugと技術名の整合性まで見る。
  • 新しい記事を書く時は、ファイル名・記事タイトル・topicsを一貫した技術テーマで設計する。
  • CIが落ちても外部公開を先に進めず、失敗ステップとログを確認してから修正する。
  • QiitaへのPOST成功、Buffer投稿登録成功、Xへの実配信完了はそれぞれ別の完了条件である。

検証・ファクト確認用メモ

  • 初回CI:38100268152 で全記事テーマ検証失敗。
  • タイトル修正後CI:38100302836 成功。
  • PR #19:mainへsquash merge成功。
  • 公開Workflow:38100329857 成功。
  • Qiita:新規投稿成功(49a024290d2678e0d2f6)。
  • Buffer:X投稿作成登録成功(実配信完了は未確認)。
  • 仕様の確認元:scripts/article_metadata.py、scripts/validate_articles.py、scripts/tests/test_article_validation.py。

外部投稿の重複防止と、記事・更新履歴の仕様検証

reliability未リリース

外部投稿の重複防止と、記事・更新履歴の仕様検証

リリース状態

この記録の作成時点では未リリース。修正ブランチ上で実装・検証し、PRのレビューとCI確認を経てmainへ反映する。実際の反映結果は、リリース確認後の更新履歴に追記する。

何を変えたか

  • 通常記事とニュースのX投稿に同じ文字数計算を使用する。日本語、短いURL、長いタグを含めて予算を決め、タイトルや見出しを省略する。
  • Bufferへ送信する前にGitHubへ処理中の記録を保存し、外部投稿の結果が不明なときは自動再送しない。
  • 公開済み記事のURLと投稿IDを保持しながら、本文テーマに合う管理用識別子を追加する。技術テーマの明確な不一致と日付を除いた識別子の重複を検証する。
  • 更新履歴のtype、status、importanceの値と意味をJSONに集約し、不正値をCIで拒否する。
  • ニュースはWebに本文を掲載し、Xへリンク付き告知を行うという実装にREADMEを合わせる。
  • 投稿処理の回帰テストを、外部への実投稿なしでPRとmainのCIから実行する。

なぜ変えたか

通常記事はPythonのlenで投稿文を判定し、ニュースは日本語などを重み2で判定していた。同じ日本語タイトルでも経路によって上限の扱いが違い、投稿失敗の原因になっていた。

また、従来はBufferの投稿成功後に実行環境のJSONを更新し、最後にgit pushしていた。投稿が成功していてもJSON保存やpushが失敗すれば、次の実行は未投稿と判断してしまう。外部サービスへの送信とGitHubへの記録は、1つの処理としてまとめて成功・失敗させられないため、順序と不確定状態の扱いが必要だった。

記事と更新履歴のチェックも、ファイル名の意味やメタデータの許容値までは確認していなかった。

技術的な内容と設計判断

投稿前の永続化と、結果不明時の停止

記事パスをキーにして、attempt_id付きのpending状態をGitHub Contents APIへ保存する。現在のファイルSHAを指定して更新し、競合した場合は最新状態を読み直す。このように「読んだ時点の値がまだ同じ場合だけ更新する」方式を比較更新という。

保存成功を確認した実行だけがBufferへ送信する。成功後にpostedへ更新できなくても、GitHubにpendingが残るため、新しい実行が自動再送することはない。

Bufferの公開createPost仕様には、この実装で利用できる再送時の重複排除キーを確認できなかった。そのため「必ず1件投稿される」保証はしない。投稿数が0件で止まる可能性を受け入れ、結果不明時の重複を避ける。復旧時は既存の投稿IDを照合して確定するか、元の実行終了と未作成を確認してから再試行を許可する。一定時間でpendingを自動解除する設計は採用しなかった。

公開URLと、内容を表す識別子を分ける

2026-09-26の記事はPostgreSQLがテーマなのに、GitHub Actionsを示すファイル名で公開されていた。Zennでは公開済みslugを変更すると別記事になり、chicchi.workもファイル名からURLを生成する。そのため、単純な改名は既存の投稿とリンクを損なう。

公開ファイル名とQiita・BufferのIDを保持し、content_slugを検証と重複テーマ判定で使用する。例外は専用の対応表に限定し、未知の例外や投稿IDの欠落・変更を拒否する。旧語句が公開URLに残る制約は、記事の注記と運用文書にも明記した。

この検証は既知の技術名による明確な不一致を検出する。記事全文の意味や正確性を自動判定するものではない。

独立した検証用CIだけでは、同時に動く公開処理を止められない点にも注意した。公開ワークフローの外部API呼出し前に同じ検証を置き、保護対象の記事は削除用CLIの入口でも拒否する。通常記事の削除動作は維持する。

文字数は両経路で同じ安全側の見積もりにする

ASCIIを1、それ以外を2とし、URLにも最低23の重みを確保する。長いURLのパスや複数のUnicode文字から成る絵文字はXより多めに数え、上限を260に設定する。必須URLを残し、任意のタグを除外してからタイトルを省略する。省略時も結合文字や絵文字の途中で切らない。

Xの正確な残り文字数表示とは異なる。必須URLだけで内部予算を超えるときはURLを切らずに停止する。

値の定義を1か所に集め、既存記録は保持する

type、status、importanceの許容値・意味・同義語をproducts/update-metadata.jsonに定義する。例えばfixはbugfixと同義、shippedはreleasedと同義として扱える。statusにはリリース済みかを示す真偽値を持たせる。

既存107件で使われている語彙を維持し、READMEの表とテンプレートを同じ定義に合わせる。既存ファイルの一括書換えは行わず、未知値・誤字・複合値などを新たに拒否する。my-cardの現行consumerはtypeを文字列で表示し、status・importanceによる分岐を行っていないため、既存値を維持した変更で互換性を保てる。

検証

外部APIをテスト用の応答に置き換え、正常投稿・投稿前の保存失敗・投稿成功後の記録失敗・同時実行・既存投稿のスキップ・下書きの除外を確認する。加えて、日本語や短いURLを含む上限、更新履歴の許容値、既存Qiita IDへの更新のみが行われることを検証する。

全記事・ニュース・更新履歴をCIで検証する。実際のX投稿をテストデータで作成する確認は行わない。

統合後のローカル検証では77テスト、記事29件、ニュース関連19ファイル、更新履歴109件がすべて通過した。ニュース関連19ファイルは本文17件とREADME・テンプレートを含む。更新履歴は開始時点の既存107件に、並行作業で追加された1件と本記録1件を加えた数である。

独立レビューでは、日本語に隣接する短いURLの検出漏れと半角濁点の分断を再現して修正した。投稿状態ファイルの欠落も未投稿とは判断せず停止する。HTTPの安全なエラーコードは残し、応答本文や任意の例外に含まれる認証情報はログへ出さない。

記事公開処理・メタデータ修正のmain反映を確認(リリース済み)

releaseリリース済み

記事公開処理・メタデータ修正のmain反映を確認(リリース済み)

リリース状況

2026-10-07にPR #13をmainへマージした。反映コミットはa01e8f7abab8f634415772012dbc949302d4c292。mainの投稿処理と検証ワークフローへ適用済みで、4ワークフローすべての成功を確認した。Issue #4・#5・#6・#7・#10は修正PRによりクローズされた。

適用内容

通常記事とニュースのX文字数判定を共通化し、GitHub上へ送信前に投稿状態を保存する。公開済み記事のURL・投稿IDを保持したテーマ検証、更新履歴の共通値定義、ニュース告知仕様の文書化、77件の回帰テストを適用した。

原因調査、判断理由、復旧方法の設計は、同日の 2026-10-07-publishing-reliability-and-metadata.md に記録した。同ファイルの「未リリース」は実装時点の状態として残し、本記録で反映結果を追記する。

確認した結果

  • mainの回帰テストと全コンテンツ検証: 成功。77テスト、記事29件、ニュース関連19ファイル、更新履歴109件が通過した。この件数は本リリース記録を追加する前の値。
  • mainの公開処理: 成功。記事の事前検証、Qiita同期、Xの投稿済み判定が完了した。
  • mainのニュース検証: 成功。
  • mainの更新履歴検証: 成功。
  • 対象の9/26記事は既存のQiita記事ID・Buffer投稿IDを保持している。投稿状態ファイルの内容もマージ前と一致し、追加のX投稿は作成されていない。

技術的な知見

検証用ワークフローと公開用ワークフローは独立して動くため、CIが不正データを検知するだけでは外部書込みを防げない。公開処理の入口と削除用CLIの入口にも保護条件を適用した。

また、公開処理でBufferを呼ぶ前にリモートの状態保存を完了させることで、runnerが失われても次の実行が処理中の記録を参照できる。成功後だけローカルに保存して最後にpushする方式では、この境界を守れない。

運用上の制約

結果不明のpendingは自動再送せず、READMEの手順で既存投稿の照合、または元実行終了と未作成を確認して復旧する。Buffer上の投稿作成とX上の配信完了は別の状態として扱う。公開済み9/26記事の旧URLは互換性のため維持する。

今回、外部APIへテスト用のX投稿を作成する操作は行っていない。障害・並行実行・再実行はローカルHTTPサーバーと別Pythonプロセスで再現した。