台風の接近で朝からかなりの雨です・・・甲斐犬のキクの散歩は予定通り、上下に登山用のレインコートを着込んで散歩、でも40年前のゴアテックスは効果が薄く、かなり浸み込んできました・・・やはり、新しいのを使わないと大雨は無理かな


環境側面であるコードのレビューを実施した
\environment\edit_environment.blade.php
\environment\index.blade.php
1.最優先:CSPとインラインハンドラの矛盾
これが一番の地雷です。直近で@alpinejs/cspに移行してscript-srcから unsafe-eval を外したのに、1-1.この2画面はインラインイベントハンドラだらけonclick=”openAiModal()” / “closeAiModal()” / “runAiEstimate()”
onclick=”addCustomRow()” / “removeCustomRow(this)”
onclick=”window.print()”
script-srcからunsafe-inlineも外す方針なら、これらは全滅
Alpineだけ対応しても、素のonclickが残っていればCSPの一貫性は崩れる・・セキュリティ監査を目標にするなら、unsafe-inline除去は通過点になるはずなので、ここはaddEventListener方式へ統一すべき・・幸い既に477〜492行で同パターンを使っているのでdata-action=”open-ai-modal”のような属性+委譲リスナーに寄せれば機械的に直せる
2.設計上の問題点
2-1.評価点計算が4箇所に散在(single source of truth がない)
floor(general × coef / 100)が PHP(135行)、JS係数入力(483行)、JS applyAiResults(588行)、さらにcustomは重み[3,2,2,2,1]をJSにハードコード(679行)して general を再計算しています。マスタのgeneral_scoreがどう作られているか不明ですが、重みの定義がDB・PHP・JSに分散している以上、評価式を変えた瞬間に表示と保存値がズレます。重み配列と計算式を1箇所(config or 計算クラス)に集約し、JSはそれを@json()で受け取る形が現実的です。
2-2.ビューにビジネスロジックが漏れている
index側のsignificantRows構築(マスタ+custom抽出・ソート、約50行の@php)と、edit側のsiteScore/aspectName解決ロジックは、本来Controllerかview-modelの仕事・・Bladeはレンダリングに専念させ、整形済みコレクションを受け取るだけにすべき・・・テスト可能性も上る
2-3.タブ定義が二重管理
$tabDefs(22行)と$tabGroups(87行)がキー(AT_WE 等)一致を暗黙の前提に別々で定義されています。片方だけ変えると静かに壊れます。1つの定義(ラベル+コード配列)に統合してください。
2-4.マスタ行とcustom行で送信構造が非対称
マスタはsignificant_ids[] / coefficients[id] / activities[id] / aspect_names[id]の並列配列、customはcustom[idx][…]のネスト構造。サーバ側の受け取りが2系統になり、バリデーションも二重になります。可能ならマスタ側もaspects[id][coef]のようなネストに揃えると、サーバ側ロジックが一本化できます。
5.共通化すべきコード(メンテ性)
最も効くのは次の2点です。
5-1.custom行HTMLの二重メンテ
— PHPの@foreach版(237〜265行)とJSのtr.innerHTMLテンプレート(635〜663行)がほぼ同一構造を二重定義しています。
列を1つ足すたびに両方直す必要があり、典型的なバグ源です。要素を1つ置いてcloneNodeする方式に統一すれば、構造の定義が1箇所になります。
5-2.grayRow/restoreRowの二重定義
— グローバル(403〜410行)とOMContentLoaded内(443〜450行)に同名関数が重複しています。グローバル版1つに統一してください。
その他、index/editで著しい側面の行レンダリングが酷似しているので、<x-environment.aspect-row> のようなBladeコンポーネント化も検討余地ありです。
6.着手するなら優先度はこの順です
- インラインハンドラ → addEventListener統一(CSP一貫性。今の方針と直接矛盾している)
- 評価点計算の集約(表示と保存のズレを根絶)
- custom行HTMLの二重メンテ解消(<template>化)
- AI上書き前の確認+係数clampの非対称解消
1と2は単一ファイル内で完結しにくく(config/計算クラスに波及)、3と4は単一ファイル修正で対応可能です。Claude Codeへ投げる前の切り分けとしては、3→4を先に単発で潰し、1と2は別タスクとして計画するのが現実的です。
3と4を、それぞれ「何が起きているか」から噛み砕いて説明します。
6-3.custom行HTMLの二重メンテとは
「新規設定」タブの1行ぶんのHTMLが、まったく同じ構造で2箇所に書かれているという話です。
ひとつ目はPHP(Blade)側、237〜265行。これはDBに保存済みの行を画面に出すためのものです
php @foreach($customAspects as $ci => $custom) …チェックボックス… …以下、有害/有益・A〜E・係数・活動項目 … @endforeach
ふたつ目はJavaScript側、635〜663行。これは「+行を追加」ボタンで新しい空の行を作るためのものです
javascript function addCustomRow() { tr.innerHTML = …チェックボックス… … 以下、有害/有益・A〜E・係数・活動項目 …; }
中身を見比べると、列の並び・`name` 属性・`class` がほぼ瓜二つです。違うのは「値が入っているか(既存行)/空か(新規行)」だけ。
**何が困るのか。** 例えば将来「備考」列を1つ増やすとします。すると、PHP側とJS側の**両方**を同じように直さないといけません。片方を直して片方を忘れると、「保存済みの行には備考欄があるのに、新規追加した行にはない」という食い違いが起き、しかもエラーは出ないので気づきにくい。これが「二重メンテ」の怖さです。
**どう直すか。** HTMLの「ひな形」を画面に1つだけ置いておき、新規追加時はそれを複製(コピー)して使う方式にします。`<template>` というタグは、中身が画面に表示されない“型紙”専用の入れ物です。
“`html
{{– 画面には出ない型紙。1行ぶんのHTMLをここに1回だけ書く –}}
<template id=”custom-row-template”>
<tr class=”bg-white border-b border-gray-200 custom-row”>
<td>…チェックボックス…</td>
<td><input name=”custom[__IDX__][aspect_name]” …></td>
…
</tr>
</template>
“`
“`javascript
function addCustomRow() {
const tpl = document.getElementById(‘custom-row-template’);
const tr = tpl.content.firstElementChild.cloneNode(true); // 型紙を複製
const idx = customRowIndex++;
// name属性の __IDX__ を実際の番号に置換
tr.querySelectorAll(‘[name]’).forEach(el => {
el.name = el.name.replace(‘__IDX__’, idx);
});
document.getElementById(‘custom-tbody’).appendChild(tr);
// スコア入力のイベント登録(従来どおり)
}
“`
これでJS側の長いテンプレート文字列が消えます。完全に1箇所化はできません(既存行は値入り、新規行は空、という違いがあるため)が、**列の構造を変えるときに直す箇所が大きく減り**、JSとPHPの食い違いが起きにくくなります。
3.バグになりそうな点
3-1.customRowIndexのキー衝突リスク(要確認)
`js let customRowIndex = {{ $customAspects->count() }};
`@foreach($customAspects as $ci =>$custom) のキー `$ci` が `0..n-1` の連番である保証がないと衝突します。Controllerで `filter()` 等を通したコレクションだとキーが歯抜けになり、`count()` が「次の安全なindex」になりません。Controller側で `->values()` 済みか確認してください。不安なら `customRowIndex` を「既存キーの最大値+1」で初期化する方が堅牢です。
3-2.AI判定で手入力値が無確認で全リセット
applyAiResults` は全行の係数を一旦0にしてからAI結果で上書きします(571〜576行)。仕様ならよいですが、確認ダイアログがありません。手入力済みの現場で誤クリックすると消えます。実行前に「現在の入力を上書きします」の確認を1枚挟むべきです。
3-3AIレスポンスの値域検証が弱い
parseInt(coefficients[aspId]) || 0だけで”50%”や小数、範囲外(>100)が来た場合の clamp がありません。係数入力欄のリスナー(482行)ではMath.min(100, Math.max(0, …))しているのに、AI反映側(581行)では clamp していない非対称があります。AI出力はフロントでもサーバでも信頼せず clamp すべきです。
3-4.JS再計算とDBのsite_score表示がズレうる
初期表示はDBのsite_score、編集すればJS式で再計算。両者の式が一致している保証がコード上ありません(上記「計算の集約」とセット)
4.セキュリティ(商用観点)
4-1.AIへ送る情報の制御がフロントにない
ai_client_env_requirement等のtextareaを丸ごと送信します。「AIに投げる情報をAIで管理」の方針からすると、最低限フロントにmaxlength、サーバ側に文字数上限・機密語検出を入れる余地があります。
– テナント分離:ビュー単体では判断できませんが、$project->idルートに対する所有権チェックがPolicy/Controllerで効いているか要確認。マスタ全件がビューに渡っている点も、他社データ混入がないか設計レベルで担保が必要です。
– XSSは{{ }}のエスケープで概ね守られており、AI反映もtextContent/数値なので低リスク。ここは問題なしです。
catchでerr.messageやres.statusTextをそのままalert表示しているのは軽微な情報露出。利用者向けには定型文に寄せるのが無難です。
6-4.これは実は2つの別問題です
「AI上書き前の確認」と「係数clampの非対称」は別物なので分けます。
4-a. AI判定で手入力が無言で消える
AI判定を実行すると、applyAiResults(571〜576行)が画面の全係数を一旦ゼロにリセットしてからAIの結果で埋め直します。
問題は、その前に何の確認もないこと。現場の人が係数を手で入力したあとに、うっかりAI判定ボタンを押すと、入力が全部消えます。やり直しもできません。
直し方は1行です。 AI判定の入り口で「いいですか?」と聞くだけ
javascript function runAiEstimate(){ if (!confirm(AI判定を実行すると、現在入力済みの係数はすべて上書きされます。続行しますか?)) { return; // キャンセルなら何もしない } closeAiModal(); // …以降は従来どおり }
### 4-b. 係数の「clamp」が片方だけ抜けている
まず用語。clamp(クランプ)とは、値を決められた範囲に押し込める処理です。係数は0〜100が正しいので、「マイナスが来たら0にする」「100を超えたら100にする」という安全装置のことです。
人が手で入力する欄では、ちゃんとこの安全装置が効いています(482行)
javascript const coef = Math.min(100,Math.max(0, parseInt(this.value) || 0)); // 0未満→0、100超→100 に丸める
ところが、AIの結果を画面に反映する側(581行)では、これが抜けています。javascript const coef =parseInt(coefficients[aspId]) || 0; // 安全装置なし。AIが 120 を返せば 120 がそのまま入る
これが「非対称」という言葉の意味です。同じ「係数をセットする」のに、人が入れたときは守られて、AIが入れたときは無防備という不揃いがある。AIは普通は0〜100で返すはずですが、想定外の値を返したときに画面に変な数字が出てしまいます。
直し方は、同じ安全装置をAI側にも通すだけ。 ついでに、この丸め処理は今や3箇所目になるので、小さな共通関数にしてしまうのが綺麗です。javascript // 共通の安全装置(1箇所に定義)function clampCoef(value) { return Math.min(100, Math.max(0, parseInt(value) || 0)); }
“`javascript
// 手入力側(482行)
const coef = clampCoef(this.value);
// AI反映側(581行)
const coef = clampCoef(coefficients[aspId]);
“`
これで両方が同じルールで丸められ、AIが変な値を返しても0〜100に収まります。
整理すると、4-aはconfirmを1行足すだけ、4-bはclampCoefという小さな関数を作って2〜3箇所を差し替えるだけです。どちらもこのファイル1枚で完結します。3の<teplate>化はやや手数がありますが、列の追加に強くなる投資です。


