Reports

TL;DR 在庫CSV/Excel をドロップしてから取り込み確定まで、9ステップを順に追う。テンプレ一致は従来ウィザードへ、不一致だけ AI 解析。解析結果は コードが矯正し、迷う列だけ 1問ずつ会話で確認。確定すると整形済みデータが 既存ウィザードに流れ込み、取り込み成功後に 再利用ルールを保存する。

目次 §1 9ステップ俯瞰 §2 ①②ドロップと前処理 §3 ③解析 §4 ④矯正 §5 ⑤⑥列計画と会話 §6 ⑦適用 §7 ⑧⑨既存ウィザードと確定 §8 会話の状態機械
§1 9ステップ俯瞰
AI が判断 コードが決定的に処理 DB
① ドロップ
CSVImportModal
② 前処理
dropEmptyColumns
③ 解析
analyze API
④ 矯正
sanitizeAnalyzeResult
⑤ 列計画
buildColumnPlans
⑥ 会話確認
AiImportThread
⑦ 適用
applyAnalysisToRows
⑧ 既存ウィザード
useSpreadsheetImport
⑨ 確定保存
commit API

「AIが触るのは③だけ」なのが見て取れます。残りは全部コードの決定的処理。これが原則1(AIは提案だけ)の実物です。

§2 ①ドロップと振り分け / ②前処理
ファイル投入 → テンプレ判定で振り分け
nanco の標準テンプレに合致するなら、AIを使わず従来のウィザードへ直行(速い・無料)。合わない雑なデータだけ AI 経路に入る。
detectStockTemplateMatch で判定。不一致なら本体シートが AiImportThread に切り替わる。
AI同意の確認(会社ごと初回だけ)
手持ちデータを外部AIに送るため、会社ごとに1回だけ「AIによる解析を使用します」と同意を取る。
CompanyDB.aiImportConsentedAt が null なら同意ダイアログ。同意保存に失敗したらAIを開始しない(fail-safe)。
空列を落とす+列統計を集計
全データ行で空の列は丸ごと除外。ただし黙って消さず、後で「空の列◯◯は取り込みません」と1回知らせる。各列の型推測・充填率・サンプル値も集計しAIに渡す準備。
dropEmptyColumns(table) → {table, droppedColumns[]}。列統計 ColumnStat[] をクライアントで作る。
全行をAIに渡さないのは、コストとハルシネーション対策(原則1)。代わりに「ヘッダー+サンプル最大50行+列統計(型・充填率・ユニーク例・件数)」という要約だけ渡す。要約を作るのはクライアント側のコード。
§3 ③解析(AIが触る唯一の場所)
「前回と同じ形式」を先に探す
解析の前に、過去に同じ列構成で取り込んだ実績がないか lookup。あれば「前回と同じ形式のようです」と提案し、列の質問をまるごと省ける。
lookupReuseApi(LLM不使用)。詳細は ⑤再利用。
解析アニメ(作業の可視化)
「列を読み取り中/アイテム情報と見比べ中/整える準備中/確認することを整理中」を順に表示。ただの待ち画面でなく、何をしているか見せる演出。最終ステップだけは実際のAI応答が返るまで完了させない。
AiAnalyzingCard。ステップは700ms間隔、最終ステップは done=true まで待つ。
解析リクエスト → AIが取り込み方針を返す
会社の既存フォルダ・アイテム情報を含めたプロンプトで、列の意味・新規属性提案・価格列・PII列などを構造化出力で受け取る。
analyzeImportApi → サーバーで aiClient.structured(..., AnalyzeResultSchema, {tier:'strong'})。OpenAI Responses API・store:false(会話を保存しない)。
商品名をアイテム名として扱います。原価は仕入値として登録します。メーカーは新しい情報「メーカー」として保存します。全 5 行を読み取りました。

この要約文(summary)はAIが生成。AIからの「お知らせ」のうち confirmations.kind==='other' だけが確認ピルとして会話に出ます。

§4 ④矯正(AI出力を信じず検算する関門)

AIの構造化出力は「型」は合っていても「意味」が正しいとは限りません(存在しない列番号、架空の属性名など)。sanitizeAnalyzeResult が行データに触る前に、決定的に直します。

矯正内容
範囲外の除去列数を超える sourceIndex を持つマッピング・ルールを捨てる。
架空属性名の救済会社に存在しない属性名を指す列は ignore に落とすが、新規提案として可視化(黙って消さない)。
nancoID の厳格判定他システムの管理番号(例 N0010-A)を nancoID 扱いすると全行が照合エラーになるため、サンプルの8割以上が nanco 発番形式でなければ「管理番号」属性へ降格。
価格列の補完AIが取りこぼして ignore に落とした「単価・金額」系の数値列を、見出し正規表現+全行型判定で価格列に拾い直す。
消える列の救済データがあるのに ignore された列を opt-in 提案(既定オフ)として出す。原則2の番人。
「列統計(全行を見た情報)」が渡された時だけ救済が働く。AIに渡すサンプル値は上限で切り詰められるので、AIの判断より「全行を見たコードの型判定」を信頼する、という割り切り。
§5 ⑤列計画 / ⑥会話で1問ずつ確認

buildColumnPlans が各列を5種に分類。auto 以外だけが質問に回ります。明確な列(システム列・既存名一致+整形不要+高確信≥0.7)は黙って確定。

種別意味カードの選択肢
auto確定済み(質問しない)—
newAttr新しいアイテム情報の提案「◯◯として取り込む/取り込まない/別の情報にする」+型ピッカー+自由入力
rescue整形すれば使える「整形して取り込む/取り込まない/別の情報にする」
ambiguous意味が曖昧「◯◯として取り込む/取り込まない/別の情報にする」
price価格列「販売価格/仕入価格/この列は使わない」(権限なしは ignore 固定)

列カード(型ピッカー+自由入力)

新規属性の列では、種類(文字/数値/日付/選択肢)をピルで選べ、おすすめには「(おすすめ)」と理由を一言添える。選択肢型なら選択肢チップを編集でき、型が合わなければ(ブロックせず)警告。さらに自由入力で「数値にして」「選択肢にして 赤 青 緑」「これは商品名」などと指示でき、AIが1ショットで解釈し直します。

「メーカー」列、どう取り込みますか?
メーカーとして取り込む取り込まない別の情報にする
文字数値日付選択肢(おすすめ)
確認文や型のおすすめに必ず「理由」を添えるのは、結論だけだと判断しづらいから。プロダクトのAI応答も同じ方針(「何を・なぜ」をセットで返す)。

進め方は「1つずつ」か「一気に」を最初に選べ、質問が5列を超えると「まだ続けますか?」と一度区切ります(cap=5)。「一気に」を選ぶと質問を飛ばし、価格列はAI推測ロールを自動適用します。

§6 ⑦適用(全行を正規の表へ)

会話の回答は AiImportThread の決定ストア(attrDecisions / overrides / priceDecisions / rejectedTransforms / folderSimplify)に溜まります。これらを後勝ちで合成した最終方針(effectiveResult)を applyAnalysisToRows に渡し、全行を整形します。

できた表は checkCsvHeader(既存ウィザードと同じ検証)にかけてから親へ返します。「AIは提案、変換は決定的コード、検証は既存と同じ」が1つの関数(trial)に集約されています。

§7 ⑧既存ウィザードへの注入 / ⑨確定とロールバック
新規属性を実際に作成(必要な分だけ)
本当に使う新規属性だけを作成(ignoreや別情報にした列・既存同名は除外)。価格ロール属性→提案属性の順に逐次作成。
handleProceed。作成IDを蓄積し、途中中断ならロールバック。Promise.all禁止(採番のデッドロック対策で逐次)。
onComplete で整形済みデータを親へ
正規化テーブル+作成した属性ID+commit用ペイロード(再利用ルール)+伝票ヘッダーを返す。
onComplete(checkCsvHeader(...), createdIds, {sessionId, decisions}, documentFields)
既存ウィザード(STEP4)へ流し込む
AI出力は「ただの正規化済み表」として既存の賢い表プレビュー=取り込みロジックに渡るだけ。AI専用の特別扱いは無い。
CSVImportProgress(initialStep=4)→ CSVTable → useSpreadsheetImport。
取り込み成功 → commit で再利用ルールを保存
取り込みが成功した時だけ commit を1回。失敗したデータを再利用に残さないため「開始時」でなく「成功時」に発火。
importSucceeded 発火で commitImportApi(親 CSVImportModal が保持)。
孤児防止: 取り込まずにシートを閉じた場合、AIが作成した新規属性を rollbackCreatedAttrs で削除し、使われない属性が残らないようにする。
§8 会話の状態機械(実装の勘所)

会話は msgs: Msg[](メッセージ配列)で駆動。回答ハンドラが次のメッセージを push することで前進する手続き的な状態機械です。種類は file / analyzing / prose / echo / confirm / emptyColumns / reusePrompt / folderPlan / preamble / columnQuestion / capPrompt / mapping / fatal。

once ガード ref
*AnsweredRef / *DecidedRef で各確認の再発火を防止。回答済みピルは固定表示。
remount 回避
列カードは <X/> でなく関数呼び出しで描画。入力欄はローカルstate。フォーカス飛び対策。
二重送信防止
自由入力は同期 interpretBusyRef で即弾く。finallyで必ず解放(漏れると2回目以降が無言で死ぬ)。
IME対策
Enter送信時に isComposing と keyCode!==229 を両方見て、日本語変換確定のEnterで誤送信しない。
中断(AbortSignal)
1つの AbortController を生存判定に使い、シートを閉じたら数十秒のfetchを中断。
StrictMode耐性
解析は1回だけ開始、abortは setTimeout 越しに「本当のunmount」だけ実行。再マウントなら中断をキャンセル。