HANDOVER
HANDOVER
EC-CUBE 4.3 の ALPS プロファイル構築と、Be Framework + BEAR.Sunday への移植 Pilot の進行記録。次の AI セッション (および人間レビュアー) への引き継ぎメモ。
| メタ | 値 |
|---|---|
| Last updated | 2026-05-21 |
| Latest session | phase-b-slice-5-env-gated-entry-point-opus-4.7 |
| Scope | ALPS プロファイル + Be/BEAR 移植 Pilot (Pilot 1 goProduct / Pilot 2 doAddCartItem — Cascade / Pilot 3 doConfirmOrder — Branching + 4 段 Linear Cascade / Pilot 4 doRegisterCustomer — Multi-Reason Being / Pilot 5 doCheckout — Diamond-Cascade + Multi-side-effect Final) + alps-to-be-bear skill dogfooding + Phase B Slice 1-5 (Psalm setup / ProdModule / Mass-assignment fix / env-gated entry point) |
このファイルは元
handover.jsonを Markdown 化したもの (2026-05-17)。スキーマ未定義のまま JSON で運用していたが、機械処理する場面がなく自然言語のnoteが多いため Markdown へ移行した。
2026-06-01 注記: 本ファイルは構築プロセスの歴史的ログとして残す。現在の到達点・数値・残作業は
migration-status.mdが正。本文中の古い transition 数、storage 数、stub 数は当時のスナップショットとして読む。
プロジェクト名と monorepo レイアウト (2026-05-17)
旧 MyVendor.EcCube (移植 pilot 用の作業 repo) を BeMart へ改称し、BeMart repo 内の monorepo に統合した。
- 製品コンセプト: BeMart = AI 駆動の EC-CUBE → BEAR.Sunday + Be Framework 全置換プロダクト。「Be (変容による存在) で生まれ変わる Mart」
- 改称理由: (a) 旧名は EC-CUBE 商標を連想させる、(b) Pilot 1+2 完了で「単発の参照実装」から「製品候補」へ昇格した
レイアウト
BeMart/ ← BEAR.Sunday アプリ (top)
├── composer.json ← my-vendor/be-mart (path repo で be-mart-be を ref)
├── phpunit.xml ← bemart + bemart-be 2 testsuite
├── src/
│ ├── Resource/Page/... ← MyVendor\BeMart\Resource\Page
│ └── Module/ ← MyVendor\BeMart\Module (AppModule + DevBecomingProvider)
├── tests/Resource/ ← MyVendor\BeMart\Tests\Resource
├── bin/, public/ ← BEAR 実行 entry
├── var/log/, var/tmp/ ← BEAR runtime data
├── alps.json, docs/, ... ← ALPS 公開成果物 (従来通り)
└── be/ ← Be ドメインライブラリ
├── composer.json ← my-vendor/be-mart-be (library)
├── src/{Input,Final,Semantic,Exception,Becoming,Reason}/ ← MyVendor\BeMart\Be\*
├── tests/Domain/ ← MyVendor\BeMart\Be\Tests\Domain
└── var/{fake,schema,analysis}/ ← Be domain fixture
namespace 関係: MyVendor\BeMart\ (BEAR) ⊃ MyVendor\BeMart\Be\ (Be domain)。BEAR は Be に片方向依存。Be は framework-agnostic を保つ (DI で path/Becoming は inject)。
開発と将来の packagist 切り出し
- 開発中は composer の
repositories: [{type: "path", url: "be", symlink: true}]でvendor/my-vendor/be-mart-beに symlink。修正は即座に top から見える - packagist 公開時は
git subtree split --prefix=be -b release/beで別 repo に分離し、my-vendor/be-mart-beを通常依存に切り替え be/単体テストは現状 top の AppModule に依存 (Domain test がnew AppModule(new Meta('MyVendor\\BeMart', 'test')))。packagist 切り出し時は Be ライブラリ独自の test bootstrap を作る必要あり
Pilot 1/2 履歴の参照先
- 旧パス:
~/git/MyVendor.EcCube/(削除済み) - 新パス:
~/git/BeMart/(BEAR top) +be/(Be domain) - 以下 Pilot 1/2 セクションの「リポジトリ」欄も新パス前提で読む
完了済み作業
Ontology (276 semantic descriptors)
182 src-entity + container 型。タグ: src-entity / src-template / src-router / src-controller。
Taxonomy (73 states)
40 既存 + 17 新規 (フロントエンド) + 16 新規 (admin)。新規追加:
Top, ShoppingLogin, ShoppingNonMember, Shopping, ShoppingShipping, ShoppingShippingEdit, ShoppingConfirm, ShoppingComplete, ShoppingError, CustomerRegistrationComplete, Mypage, MypageHistory, MypageChange, MypageWithdraw, HelpAbout, HelpGuide, HelpAgreement, HelpPrivacy, HelpTradeLaw
Choreography (137 transitions)
safe: 58 / unsafe: 35 / idempotent: 44。全 transition が src-router タグ付き + doc 付与。
新規 transition: goTop, goLogin, doCopyProduct, doBulkUpdateProductStatus, goExportProduct, doImportProductCsv, doImportCategoryCsv, goExportCategory, goShopping, goShoppingLogin, goShoppingNonMember, doSubmitNonMember, goShoppingShipping, goShoppingShippingEdit, goShoppingShippingMultiple, doSelectShippingAddress, doUpdateShippingAddress, doConfirmOrder, goShoppingError, doBulkDeleteOrder, goExportOrder, goExportShipping, doImportShippingCsv, goExportOrderPdf, goMypage, goMypageHistory, goMypageChange, goMypageWithdraw, goExportCustomer, goHelpAbout, goHelpGuide, goHelpAgreement, goHelpPrivacy, goHelpTradeLaw
修正された rt 関係:
doCheckout:#Order→#ShoppingCompletedoLogin:#Customer→#MypagedoLogout:#Login→#TopdoRegisterCustomer:#Customer→#CustomerRegistrationCompletedoUpdateCustomer:#Customer→#MypageChangedoWithdrawCustomer:#Login→#TopCartstate:doCheckout→goShopping
Quality refinement (Opus 4.7 session)
誤解を招く doc の改善、似た名前ペアの曖昧さ解消、仕様書 (doc4) 由来の拡充、ERD 由来の新規 descriptor 追加、123 件の S001 suggestion 解消。
verify-* 由来:
- 誤解を招く命名 doc 強化:
classNameLabel,classCategoryName,preOrderId,confirmUrl,secretKey,linkMethod,cartKey,deviceType,pageEditType,authority - 似た名前ペアの区別 doc:
charge/paymentCharge,taxRate/taxRuleRate,total/totalPrice/paymentTotal,deliveryFee/deliveryFeeAmount/deliveryFeeTotal,message/shopMessage - 誤記修正:
message(お問い合わせ欄),shopMessage(Help/about.twig) - バリデーション説明拡充:
kana01,kana02,pref,freeArea
doc4 由来: orderStatus (workflow rules), orderItemType (6 区分の課税/税表示), orderItemPrice, orderItemTax, taxRate, taxAdjust, roundingType, subtotal, total, paymentTotal, paymentCharge, point, applyDate
ERD 由来の新規 descriptor: addPoint, usePoint, deliveryFeeAmount, saleTypeName, taxDisplayType, taxType
Transition doc 追加: 123 件。ドメイン横断 (catalog / cart / checkout / order / customer / account / help / shop / payment / delivery / tax / content / admin-system / mail / plugin / cms / catalog-admin)。MyVendor.Cms 流の 1 文スタイル (目的 · スコープ · 主要な副作用/パラメータ)。
Provenance tags
| タグ | 件数 |
|---|---|
src-entity |
193 |
src-router |
156 |
src-template |
80 |
src-controller |
10 |
src-unknown |
0 |
Validation
| 項目 | 値 |
|---|---|
asd --lint errors |
0 |
| warnings | 0 |
| suggestions | 0 |
HTML 生成 (alps.json.html, docs/alps.json.html) |
✅ 同期済み |
SVG 生成 (alps.svg, docs/alps.svg) |
✅ 同期済み |
| Self review (Opus 4.7) | ✅ 合格 |
Route coverage
| 項目 | 値 |
|---|---|
| 総 route 数 | 250 |
| transition でカバー | 135 |
| return type としてカバー | 7 |
| API / Ajax / utility | 78 |
| 未カバー (user-visible) | 30 |
| 実効カバレッジ | 82.6% |
Pilot 1 — goProduct (Be + BEAR.Sunday 初参照実装)
目的: Be Framework + BEAR.Sunday の統合パターンを 1 件だけ確立。
| 項目 | 値 |
|---|---|
| リポジトリ | ~/git/BeMart |
| スコープ | Product container を ProductClass 平坦化形まで縮小 (#[Embed] による子リソース合成は別 Phase)。URL param は productCode (schema.org/sku 由来、ユニーク性) |
| テスト | 8 passed (Domain 5 + Resource 3), 20 assertions |
指標 (Pilot 1)
| # | 指標 | Target | Actual | Note |
|---|---|---|---|---|
| 1 | Semantic クラス数 | 5 | 4 | ProductCode, ProductName, Price02, Stock。5 件目 (StockUnlimited 等) は Pilot スコープ外 |
| 2 | 自己証明で省略できた単体テスト | 5 | 4 | Semantic 型保証分を Semantic クラスごとに 1 件省略。Final 存在 = 検証済みを ProductResourceTest が間接確認 |
| 3 | 意味ログ自動カバレッジ | 100% | 100% | DevBecoming が Becoming chain 全体 (Input prop / Final inject / close prop) を var/log/bemart.json に自動記録 |
| 4 | LoC (Be+BEAR) | 実測のみ | 576 | BEAR-only 比較は推定値とせず実測のみ記録。BEAR-only 版は未実装 |
| 5 | i18n 例外メッセージ | 100% | 100% | 6/6 例外に #[Message(['en'=>..., 'ja'=>...])] 付与 |
| 6 | 自己証明 assert | ≥1 | 1 | Resource/Page/Product.php:44 で assert($final instanceof ProductFetched) |
Pilot 1 key decisions
BecomingInterface → DevBecomingを本番でも常時 bind — 意味ログを常時取得AppModuleにAppMetaModuleをoverrideで install — テストでBEAR\Package\Modulefactory を経由せずnew Injector()で動作可能にするためProductNotFoundExceptionは Resource 層で catch してCode::NOT_FOUNDにマップ — Be の Final 閉鎖原則と HTTP プロトコルの折衷点- Be 流
FakeQueryModuleの代わりにProductQueryInterfaceを JSON fixture 読み込みのFakeProductQueryで実装
Pilot 1 で見つかった workflow prompt の穴
alps-analyze.md:bear_url_template不足 (/product/{productCode}解決方法)domain-implement.md:BecomingInterfaceの DI 経路とAppModule例の不足application-implement.md:BEAR\Resource\Code定数の参照、#[Embed]の扱い不足integration-review.md: Final 閉鎖原則 vs HTTP プロトコル例外マッピングの判断基準
Pilot 2 — doAddCartItem (Cascade: Being chain + Final convergence)
目的: Cascade パターン (Being chain + Final での Reason 収束) の初参照実装 + alps-to-be-bear skill の dogfooding。
改訂履歴 (2026-05-17 → 2026-05-18):
- 初版 → Phase 8 reflection で「Cascade Diamond」と分類
- self review 1 で「1 つの Final 内に 5 ブロックを手続き的に並べただけで、Being 連鎖も
#[Reason]収束も無い」と判明し Linear / Minimal へ再分類 (commit 8f75c66) - self review 2: 真の Cascade として refactor —
QuantityAdjustedBeing を導入し、Stage 1 (quantity 確定 + cartKey 解決) → Stage 2 (cart 文脈 + マージ + 配送 + 保存) の 2 段 cascade に。Final で 3 つの独立 Reason (CartQuery+CartCommand+ProductClassQuery) が#[Inject]で収束 (commit 35a0201) - self review 3 (2026-05-18): 「Final が永続化以外に in-memory merge + totalPrice + deliveryFeeTotal 計算を抱えていて厚い」という違和感から、
CartMergedBeing を抽出。3 段 cascade に再構成 — Stage 1 (QuantityAdjusted: ProductClass lookup / Stock cap / SaleLimit cap / cartKey) → Stage 2 (CartMerged: 既存 cart 取得 / item merge / totalPrice / deliveryFeeTotal) → Stage 3 (CartItemAddedFinal: 永続化のみ)。Final の#[Inject]はCartCommand1 つに収束。Linear 版の snapshot はbe/docs/variations/linear-doAddCartItem/に保存 (commit 8f75c66 時点のコード)。本評価はbe/docs/be-adoption-evaluation.mdに詳述。Cascade Diamond reference (be-patternsorder-processingのような複数 Moment 並列収束) は doAddCartItem の domain には不適 (quantity 確定 → cart 合成 → 永続化 は本質的に直列) で、将来の Pilot (例:doCreateOrderで Cart + Customer + Payment 並列収束) に譲る
| 項目 | 値 |
|---|---|
| リポジトリ | ~/git/BeMart |
| パターン | Cascade (3 段の Being chain + Final convergence)。AddCartItemInput → QuantityAdjusted (Being) → CartMerged (Being) → CartItemAdded (Final)。Stage 1 = ProductClass lookup + Stock cap + SaleLimit cap + SaleType 解決, Stage 2 = 既存 cart 検索 + item merge + delivery fee 集計, Stage 3 = 永続化のみ |
| テスト | 21 passed (Pilot 1 既存 8 + Pilot 2 新規 13), 51 assertions, 0 notices (Cascade refactor で導入された全 Semantic 変数を登録: SessionPrefix / RequestedQuantity / AdjustedQuantity / UnitPrice / SaleTypeId / SaleTypeName / DeliveryFee / StockUnlimited / SaleLimit / CartKey / TotalPrice / DeliveryFeeTotal / MergedCart の 13 件) |
| Skill 配置 | ~/.claude/skills/alps-to-be-bear/ (local dogfooding。Pilot 3 + 本番移植 10 件後に be-framework-skills plugin marketplace へ promote 候補) |
Pilot 2 完了の判定基準 — 12 指標
| # | 指標 | Target | Actual | Note |
|---|---|---|---|---|
| 1 | composer test 全 pass | 100% | 100% | 21/21 pass (Pilot 1 既存 8 + Pilot 2 Domain 9 + Resource 4) |
| 2 | 意味ログ被覆 | 100% | 100% | var/log/bemart.json に Becoming chain 全体が JSON で記録 |
| 3 | i18n 例外メッセージ | 100% | 100% | 全 DomainException (QuantityFormatException, ProductClassNotFoundException, OutOfStockException) に #[Message(['en'=>..., 'ja'=>...])] |
| 4 | 自己証明 assert | ≥1 | ≥1 | Final 内: assert($adjustedQuantity >= 1 && $adjustedQuantity <= $requestedQuantity)。Resource 内: assert($final instanceof CartItemAdded) |
| 5 | Semantic クラス数 | 1 | 1 | Quantity 1 件新規。ProductCode は Pilot 1 既存を再利用 |
| 6 | client-input / server-fetched 分離 | 2 シート | 2 シート | client-input (productCode, quantity) と server-fetched (stock, stockUnlimited, saleLimit, price01/02, deliveryFee, saleTypeName) を Phase 2 で分離観察 |
| 7 | LoC (Be+BEAR) | 実測のみ | src 約 620 + tests 約 250 | BEAR-only 比較は推定値とせず実測のみ記録 |
| 8 | Final の #[Inject] 数 |
(当初は Cascade 5 phase = 5 を想定) | 1 | 3 段 Cascade refactor 後の Final (CartItemAdded) は CartCommand 1 件のみ #[Inject] で受け取り、永続化に専念。Read 系の CartQuery / ProductClassQuery は Stage 2 (CartMerged) と Stage 1 (QuantityAdjusted) に分散。本質的指針: 「Final の #[Inject] 数 = Final で収束する独立 Reason 数」。Final が薄ければ Inject も少なくなる |
| 9 | 数量自動調整テスト | pass | pass | testStockShortageAutoAdjusts: sample-003 stock=3 で qty=5 → 自動補正 3, totalPrice=13500 (4500×3) |
| 10 | CartItem merge テスト | pass | pass | testSameSkuAddedTwiceMergesQuantity: 同 SKU を 2+3 で追加 → totalPrice=5000 (1000×5) |
| 11 | cartKey 分離テスト | pass | pass | testDifferentSaleTypeIsolatesCart: 通常販売 (cartKey=session-prefix-1_1) と予約販売 (cartKey=session-prefix-1_2) で cart が分離 |
| 12 | ALPS 整合性 (Rule 7) | 手動チェック合格 | 合格 (refine 後) | Final = transition outcome envelope と判明。container 状態属性 (cartKey, totalPrice, deliveryFeeTotal, saleTypeName) は ALPS Cart container に存在。transition outcome (requestedQuantity, adjustedQuantity, unitPrice) は ALPS container に不要 (操作結果)。Rule 7 を「container 状態属性 ⊆ ALPS descriptor」へ refine し、transition outcome 分離を decision-matrix.md §5 に明示 |
Pilot 2 で発生した skill gap (G-1〜G-7) — すべて昇格済み
| ID | 内容 | 昇格先 |
|---|---|---|
| G-1 | Final の #[Inject] 数 = Final で収束する独立 Reason 数 (Cascade refactor 後の指針。当初は「Cascade 段数 ≠ Inject 数」として記録 → Linear/Minimal に一旦訂正 → Cascade refactor で「Final で収束する Reason のみカウント」に確定) |
SKILL.md #6 / decision-matrix.md §4.E |
| G-2 | Reason 層の Query/Command 共有ストア (Singleton 必須) | SKILL.md #7 / decision-matrix.md §4.E |
| G-3 | Final shape は平坦が原則 | SKILL.md #8 / decision-matrix.md §4.E |
| G-4 | Pilot Input の sentinel default は本番移植で外す | SKILL.md #12 / decision-matrix.md §6.C |
| G-5 | Pilot 段階に framework-level Semantic→400 マッパー無し (Resource で明示 catch 必要) | SKILL.md #9 / decision-matrix.md §6.A |
| G-6 | Phase 6 統合 smoke を Phase 7 (PHPUnit) より前に | SKILL.md #11 / decision-matrix.md §6.B |
| G-7 | BEAR\Resource\Code に CONFLICT/GONE/UNPROCESSABLE_ENTITY 無し |
SKILL.md #10 / decision-matrix.md §6.A (整数リテラル + コメントで対処) |
Pilot 2 で更新した prompt
.claude/prompts/domain-implement.md:
- §9: Cascade 段数 ≠
#[Inject]数 セクション追加 - §9: Query/Command 共有ストア (
FakeXxxStorageSingleton) パターン追加 - §9: Diamond test snippet を平坦 Final 形 (
adjustedQuantity,totalPrice) に修正 - §8: Phase 6 統合 smoke パターン (
bin/smoke_<descriptor>.php) 追加
.claude/prompts/application-implement.md:
SemanticVariableExceptionを Resource で明示 catch (Pilot 段階で framework マッパー無し)- catch group の後に self-proof assert (
$final instanceof Final) パターン DomainException→ HTTP Code マッピング表でCONFLICT行を整数リテラル + コメント規約に変更
Orphaned states
解消済み: ShoppingError (goShoppingError transition を追加)
残存 (意図的なデータ合成パターン):
LoginHistory—LoginHistoryListの子要素 (href で参照、詳細ページなし)Template—TemplateListの子要素 (href で参照、詳細ページなし)TradeLaw—TradeLawList/HelpTradeLawの子要素 (href で参照、編集は List 内で完結)
これらは「真の孤立状態」ではなく、親 list state から href で参照されるデータ合成要素。
次の AI / 人間レビュアーへの助言
ALPS 品質
- S001 は 0。全 137 transition が MyVendor.Cms 流の 1 文スタイル (目的 · スコープ · 主要な副作用/パラメータ) で doc を持ち、全 transition doc が全角句点
。で終わる (ファイル全体で一貫) - トップレベル descriptor 数は 403 (container 266 + safe 58 + unsafe 35 + idempotent 44)。過去の handover snapshot に 413 とあったのは誤り — 403 が検証済みの値
- Self review (Opus 4.7 follow-up) で
doDeleteProduct,doCopyProduct,doUpdateCustomer,doCreateCustomerAddressの doc を強化した (元は <20 文字または入力契約のギャップあり)。他の全 transition は監査済みで受け入れ - alps.json に未反映の memo (意図的にスコープ外): SEO meta on Page (
pageDescription,pageKeyword,pageMetaRobots,pageMetaTags,pageAuthor); 顧客向け status (customerOrderStatus,customerOrderStatusName,orderStatusColorName); 国マスタ (countryName); delivery extras (deliveryRegionalFee,deliveryTimeVisible); payment extras (paymentFixed,displayOrderCount)。理由: BEAR.Sunday/Be 移植ターゲットでは優先度低 - 30 admin routes が未カバー。優先ギャップ: System Settings (8), Content Management (6), Product Class Management (5), Shop Settings (5)。追加候補 state:
AdminDashboard,SystemInfo,SecuritySettings,LogViewer,MasterData,CssEditor,JsEditor,FileManager,CacheManager,MaintenanceMode,TwoFactorAuth,CalendarSettings,OrderStatusSettings deliveryFeeAmount曖昧さ解消: 既存 ALPS id を再利用するが doc は「都道府県別の送料金額 (DeliveryFee.fee)」と明示し、基本情報側の「送料無料閾値」と区別。将来別 id が必要ならdeliveryFeeFreeThresholdを推奨LoginHistory,Template,TradeLawは意図的なデータ合成要素 (EC-CUBE 4.3 に詳細ページが存在しない)- フロントエンドの顧客向け route は 100% カバー済み
- Memo cleanup 完了: verify-misleading / verify-similar-names / verify-suffix-abbrev / verify-agent1-product / verify-agent2-order / verify-agent3-payment-delivery / verify-agent4-customer-shop / verify-agent5-admin-cms / improvements-doc4 / improvements-erd / improvements-graphql (11 ファイル、~3500 行) をリポジトリ root から
docs/quality/へ移動。improvements-graphql.md以外はalps.jsonに完全反映済み (GraphQL-API alignment は BEAR.Sunday/Be 移植ターゲットではスコープ外)。agent-1..5 は audit trail として保持
Workflow
- Phase E 完了:
alps-analyze.mdは決定論的なjson handoverfenced ブロックを末尾に emit する (descriptor_id,alps_id_resolved,alps_found,descriptor_type,be_pattern,be_reference_demo,be_classes,semantic_classes,reasons,bear,notes)。ID マッチングは 5 step の決定論的順序 (exact → lowerCamel → UpperCamel → case-insensitive LIKE → snake_case→camelCase)。type → Be 分類表を追加。domain-implement.mdとapplication-implement.mdは handover JSON を初回読み込みし、rework 時にはreviewer.blocking[]を消費する「入力契約」セクションを各々持つ。Pure-Semantic / container スキップ判定は脆い “all N/A” 文字列マッチではなくbear.skip/descriptor_typeで行う
Pilot
- Pilot 1 完了 (
BeMart(旧 MyVendor.EcCube),goProduct1 件): Be + BEAR.Sunday 統合の初参照実装が動作確認済み。意味ログ自動記録 100%、i18n 例外 100%、composer test 8/8 pass。次は (1) workflow prompt 改修 PR で Pilot で発見した穴を埋める、(2) Pilot 2 としてdoAddCartItem(Diamond パターン) を別タスクで実装、(3) 残り 135 transition を/run migrate <id>で自走移植。詳細指標は「Pilot 1」セクション参照 - Pilot 2 完了 (
BeMart,doAddCartItem1 件): Cascade パターンの参照実装。AddCartItemInput → QuantityAdjusted (Being) → CartMerged (Being) → CartItemAdded (Final)の 3 段 cascade。Final はCartCommand1 件のみ#[Inject]で受け取り永続化に専念、Read 系 Reason は上流 Being に分散。改訂履歴: 初版「Cascade Diamond」分類 → Linear/Minimal に訂正 → 2 段 Cascade refactor → 3 段 Cascade refactor (CartMerged抽出で Final から in-memory merge / totalPrice / deliveryFeeTotal 計算を引き剥がした、2026-05-18)。composer test 21/21 pass, 51 assertions, 0 notices (Cascade refactor で導入された 13 件の Semantic 変数を全登録:SessionPrefix/RequestedQuantity/AdjustedQuantity/UnitPrice/SaleTypeId/SaleTypeName/DeliveryFee/StockUnlimited/SaleLimit/CartKey/TotalPrice/DeliveryFeeTotal/MergedCart)。Linear 版 snapshot はbe/docs/variations/linear-doAddCartItem/に保存 (educational comparison 用)。Pilot 1+2 を通じた Be 採用評価 はbe/docs/be-adoption-evaluation.mdに詳述 (基層効力 4 軸 = Pilot 1 から効く / 上層効力 4 軸 = Pilot 2 で爆発)。Query/Command 共有時は Singleton ストア必須、SemanticVariableExceptionは Resource で明示 catch (Pilot 段階)、BEAR\Resource\Codeは CONFLICT 未定義で整数リテラル運用、いずれも Cascade 実装でも有効。skill~/.claude/skills/alps-to-be-bear/に昇格済み (SKILL.md / decision-matrix.md は 3 段 Cascade refactor の読み替え注記が必要)。Pilot 2 末尾の「次は Cascade Diamond を別 Pilot で」という想定は Pilot 3 で部分的に覆った —#[Input]依存の Being を#[Inject]で apex に持つ Cascade Diamond は be-framework のメカニクス上不成立 (詳細は Pilot 3 セクション /be/docs/be-adoption-evaluation.md §6)。EC-CUBE の典型的な#[Input]依存遷移は Linear Cascade に縮退する - Pilot 3 完了 (
BeMart,doConfirmOrder1 件): Branching パターンの参照実装 + Cascade Diamond 不成立の構造的発見。ConfirmOrderInput → PreOrderResolved (Being) → PurchaseFlowApplied (Being) → PaymentVerified (Being) → OrderConfirming (Being, 分岐点) → OrderConfirmed | OrderConfirmFailed (Branching Final)の 4 段 Linear Cascade + 1 段 Branching。OrderConfirmingのpublic PaymentSuccessCase|PaymentFailureCase $being型 discriminator によりBecomingType::match()が Final を選択 (be-patternsmedical-triageデモ準拠)。composer test 27/27 pass (Pilot 1 既存 8 + Pilot 2 既存 13 + Pilot 3 新規 6), 79 assertions, 0 notices (Pilot 3 で 14 件の Semantic 変数を新規登録:PreOrderId/PaymentMethodId/Subtotal/Tax/Total/Discount/Charge/DeliveryFee(既存) /AddPoint/UsePoint/PaymentTotal/Order/Totals/PaymentVerification/Being)。改訂履歴: 初版 Cascade Diamond 想定でOrderConfirmingを apex (#[Inject] PreOrderResolved $preOrder等) → Ray.DiNoHint($preOrderId)で全テスト失敗 →#[Input]依存 Being を#[Inject]経由でインスタンス化できないと判明 (Ray.Di は#[Input]属性を理解しない) → 4 段 Linear Cascade に再構成。構造的発見: Cascade Diamond は「apex Moment が#[Input]を一切持たない」場合のみ成立する。EC-CUBE の典型的フロー (Input scalar から DB 引き当て → 並列に Service 呼び出し) は全て Linear Cascade に縮退する。Pilot 3 の Branching パターンは clean に動作し、be-framework の Branching 機構 (BecomingType::match()による型ベース選択) は実用検証済み。Skill 配置:~/.claude/skills/alps-to-be-bear/のSKILL.md/decision-matrix.mdには「Cascade Diamond の成立条件 (apex が#[Input]不要) と Linear Cascade への縮退ルール」追記が必要 - Pilot 4 完了 (
BeMart,doRegisterCustomer1 件): Multi-Reason Being パターンの参照実装。RegisterCustomerInput → CustomerRegistering (Being: 4 つの独立 ReasonEmailUniquenessChecker/CustomerIdProvider/PasswordHasher/CustomerInitialPointを並列#[Inject]) → CustomerRegistered (Final: 永続化のみ)の 1 段 Multi-Reason Being + Final。Diamond と区別される構造的特徴は「各 Reason の結果が他の Reason の入力にならない (互いに独立)」こと。Pilot 4 では fail-fast query (uniqueness check) + 3 つの pure derivation (id / hash / point) が同じ Being に同居しても Diamond にはならず、blog-publishing デモのバリエーションとして成立した。composer test 39/39 pass, 111 assertions, 0 notices (Pilot 4 で 19 件の Semantic 変数を新規登録: client-input 15 件Email/Password/Name01/Name02/Kana01/Kana02/CompanyName/PhoneNumber/PostalCode/Pref/Addr01/Addr02/Birth/Sex/Job+ server-derived 4 件CustomerId/PasswordHash/InitialPoint/CustomerStatus)。スコープ決定: email 検証 OFF 経路のみ (customerStatus = 2固定)。検証 ON は将来の Branching pilot に譲る (Branching 機構自体は Pilot 3 で検証済み)。security レビュー反映: plaintext password を Being の non-public parameter +#[SensitiveParameter]で受ける (stack trace redact + 下流 public surface 不在) /CustomerIdはbin2hex(random_bytes(16))(128-bit CSPRNG)。次は (1) Complex Convergence (insurance-claimのような多分岐 + 多経路収束)、(2) admin 系の本番移植 (10 件規模) で skill を bake、(3)~/.claude/skills/alps-to-be-bear/の plugin marketplace 昇格判断 - Pilot 5 完了 (
BeMart,doCheckout1 件): Complex Convergence / Multi-side-effect Final パターンの参照実装。CheckoutInput → CheckoutPrepared (Being: 値集約のみ) → CheckoutSettled (Being: payment 副作用 + 番号採番) → CheckoutCompleted (Final: 永続化 + メール送信 + cart-clear の 3 副作用を strict order で並走)の 2 段 Being + Multi-side-effect Final。loan-application デモ準拠。composer test 52/52 pass, 149 assertions, 0 notices (Pilot 5 で server-derived Semantic 3 件OrderNo/OrderDate/PaymentDateを MergedCart パターン (空#[Validate]body) で追加。client-input 側は Pilot 3 のPreOrderId/PaymentMethodIdを再利用)。skill gap: G-14 (Ray.Di binding gotcha) —bind(Iface)->to(Impl)はbind(Impl)->in(SINGLETON)を consult しない。state を hold する Fake (FakeMailer/FakePaymentGateway等) は$obj = new Fake(); bind(Iface)->toInstance($obj); bind(Impl)->toInstance($obj);で両 binding を同一インスタンスに束ねる必要がある。AppModule.php に commented warning として記録。G-15 (Multi-side-effect Final 判定基準) / G-16 (server-derived Semantic 登録漏れ) も発見。スコープ決定: happy-path + 失敗時 422/404 のみ。Branching Final と補償処理 (Refund / InventoryRelease) は Phase B へ deferred。次は Phase B (CSRF / rate-limit / bear-security / Psalm taint / env-gated ProdModule)、または admin 系の本番移植 10 件で skill を bake
振り返り方
- 決定的要素と非決定的要素を分けて記録する。 Pilot 振り返りで「ミスがどちらの種類か」を意識する。
- 決定的要素 (規則を覚えれば機械的に処理できる) = 命名 / Semantic 登録 /
#[Input]↔ Semantic 対応 / DomainException → HTTP code mapping / Ray.Di binding 規則 (G-14 等)。ミスは過失。skill / レビュー subagent / HANDOVER の checklist で潰す層 - 非決定的要素 (ドメイン判断が要る) = Being を入れるか / 段数 / Reason 粒度 / Final に集約する副作用の数と順序 / DomainException の粒度 / Resource body の露出範囲。人間の業務知識が責任を持つ層。AI が勝手に決めると「思ったものと違うアプリ」になる (be-semantic Rule 5/6 と同じ警告)
- 決定的要素 (規則を覚えれば機械的に処理できる) = 命名 / Semantic 登録 /
- Pilot 5 の NOTICE 3 件取りこぼし (
OrderNo/OrderDate/PaymentDate未登録) は決定的要素の失敗。Ray.DitoInstance発見 (G-14) も決定的要素 (一度知ればルール化できる)。2 段 Being にした判断は非決定的要素 (「payment 前に総額を確定したい」業務観点)
Pilot 3 — doConfirmOrder (Branching + Cascade Diamond 不成立の発見)
目的: Branching パターン (Final が分岐) と Cascade Diamond (apex Moment 並列収束) の 2 つを同時に検証する設計だったが、Diamond 側は be-framework のメカニクス上不成立と判明し、Linear Cascade + Branching に縮退した。
改訂履歴 (2026-05-18):
- 初版:
ConfirmOrderInput → OrderConfirming (Cascade Diamond apex;#[Inject]で PreOrderResolved + PurchaseFlowApplied + PaymentVerified を並列収束) → OrderConfirmed | OrderConfirmFailedを想定 - Phase 7 で全 6 テスト失敗 —
Ray\Di\Exception\NoHint: $preOrderId at PreOrderResolved.php:28。Ray.Di は#[Inject] PreOrderResolved $preOrderを解決する際に$injector->getInstance(PreOrderResolved::class)を呼ぶが、PreOrderResolved::__construct(#[Input] string $preOrderId, ...)の#[Input]属性を解釈できず、コンストラクタ引数$preOrderIdを hint なしと判定して落ちる vendor/be-framework/be-framework/src/BecomingArguments.phpの挙動を確認 —#[Input]は be-framework の cascade (BecomingArguments::be(object $current, string $becoming)でget_object_vars($current)から拾う) でのみ解決される。Ray.Di のgetInstance()経路は#[Inject]のみ理解する- 構造的結論: 「
#[Input]を必要とする Being」は#[Inject]の対象になれない。Cascade Diamond の apex (#[Inject]で複数 Moment を並列収束) は、各 Moment が Input 依存ゼロの場合のみ成立する。EC-CUBE のdoConfirmOrderのように Input scalar から派生して Service 呼び出しを並列する用途は不成立 → 4 段 Linear Cascade に再構成 (各 Being が#[Input] publicプロパティで下流に scalar を forward) - 4 段 Linear Cascade + Branching に書き換え後、6/6 pass, 0 notices
| 項目 | 値 |
|---|---|
| リポジトリ | ~/git/BeMart |
| パターン | Linear Cascade (4 段) + Branching (1 段)。ConfirmOrderInput → PreOrderResolved (Being: 注文引き当て) → PurchaseFlowApplied (Being: 金額計算) → PaymentVerified (Being: 決済検証) → OrderConfirming (Being: discriminator 計算) → OrderConfirmed \| OrderConfirmFailed (Branching Final)。OrderConfirming::$being の union 型 (PaymentSuccessCase\|PaymentFailureCase) を BecomingType::match() が読み、#[Be([OrderConfirmed::class, OrderConfirmFailed::class])] から型一致する Final を選択 |
| テスト | 27 passed (Pilot 1 既存 8 + Pilot 2 既存 13 + Pilot 3 新規 6), 79 assertions, 0 notices |
| Skill 配置 | ~/.claude/skills/alps-to-be-bear/ (Cascade Diamond の成立条件と Linear 縮退ルール追記が必要) |
Pilot 3 完了の判定基準 — 12 指標
| # | 指標 | Target | Actual | Note |
|---|---|---|---|---|
| 1 | composer test 全 pass | 100% | 100% | 27/27 pass (Pilot 1 既存 8 + Pilot 2 既存 13 + Pilot 3 Domain 6) |
| 2 | 意味ログ被覆 | 100% | 100% | DevBecoming が 5 段 cascade 全体を var/log/bemart.json に自動記録 |
| 3 | i18n 例外メッセージ | 100% | 100% | 全 DomainException (PreOrderNotFoundException, PreOrderIdFormatException, PaymentMethodIdFormatException, Semantic format 系 12 件) に #[Message(['en'=>..., 'ja'=>...])] |
| 4 | 自己証明 assert | ≥1 | ≥1 | OrderConfirming::__construct() 内: $this->being = $paymentVerification->success ? new PaymentSuccessCase(...) : new PaymentFailureCase(...) で型 discriminator を自己証明 |
| 5 | Semantic クラス数 | 14 | 14 | PreOrderId / PaymentMethodId / Subtotal / Tax / Total / Discount / Charge / AddPoint / UsePoint / PaymentTotal を scalar 系として新規、composite 系 4 件 (Order for OrderEntity, Totals for PurchaseTotals, PaymentVerification for PaymentVerification, Being for union PaymentSuccessCase\|PaymentFailureCase) を MergedCart パターン (空 #[Validate] body) で登録 |
| 6 | Reason 層共有ストア | Singleton 必須 | 該当 | FakeOrderQuery を Scope::SINGLETON で bind (AppModule:50)。他の FakePurchaseFlow / FakePaymentMethodFactory は state を持たないので普通 bind |
| 7 | client-input / server-fetched 分離 | 2 シート | 2 シート | client-input (preOrderId, paymentMethodId) と server-fetched (OrderEntity, PurchaseTotals, PaymentVerification, PaymentSuccessCase\|PaymentFailureCase discriminator) を Cascade 内で分離 |
| 8 | LoC (Pilot 3 新規分) | 実測のみ | src 約 469 + tests 約 132 | Being 4 件 (164 LoC) + Final 2 件 (88 LoC) + Input 1 件 (42 LoC) + Reason Case 2 件 (43 LoC) + Test 132 LoC。Semantic / Exception / Reason Entity / Reason Query / Reason Service は別カウント |
| 9 | Branching 分岐テスト | pass | pass | testCashOnDeliverySucceeds / testCreditCardSucceeds (success path → OrderConfirmed) と testVerifyFailureBranchesToOrderConfirmFailed (failure path → OrderConfirmFailed, errors ['Card validation failed']) で双方向検証 |
| 10 | Cascade chain 整合性 | pass | pass | 4 段の #[Input] forward (preOrderId / paymentMethodId / order / totals / paymentVerification) が BecomingArguments::be() で正しく chain される。testMissingPreOrderThrows で chain 中断 (Stage 1 で例外) も検証 |
| 11 | Branching 型 discriminator | pass | pass | OrderConfirming::$being: PaymentSuccessCase\|PaymentFailureCase が BecomingType::match() で #[Be([OrderConfirmed::class, OrderConfirmFailed::class])] から正しい Final を選択。#[Input] PaymentSuccessCase $being / #[Input] PaymentFailureCase $being で各 Final が型ベースに hit |
| 12 | ALPS 整合性 (Rule 7) | 手動チェック合格 | 合格 | Final の public プロパティ (OrderConfirmed::$subtotal / $tax / $total / $addPoint 等、OrderConfirmFailed::$errors) は ALPS OrderConfirmed / OrderConfirmFailed container の descriptor と一致 |
Pilot 3 で発見された skill gap
| ID | 内容 | 昇格先 |
|---|---|---|
| G-8 | Cascade Diamond は apex が #[Input] 不要なときのみ成立 — Ray.Di getInstance() は #[Input] を解釈しないため、#[Input] 依存 Being を #[Inject] で apex に並列収束させると NoHint で fail。EC-CUBE の典型的フロー (Input scalar から派生して並列計算) は全て Linear Cascade に縮退する |
SKILL.md / decision-matrix.md §5 に「Diamond 成立条件と Linear 縮退ルール」セクション追加 (TODO) |
| G-9 | Branching pattern は型 discriminator (public A\|B $being) + #[Be([FinalA, FinalB])] + 各 Final が #[Input] A $being / #[Input] B $being でクリーンに動作。be-patterns medical-triage 準拠 |
SKILL.md / decision-matrix.md §4.F に Branching 実装テンプレ追加 (TODO) |
| G-10 | Composite 型 Semantic 登録 — PaymentSuccessCase\|PaymentFailureCase の union 型 Semantic は validate(A\|B $being): void {} で登録可。scalar に貼る Semantic と区別して「composite Semantic の登録は型断定のみ、payload contract は型自体に委ねる」と明示すべき |
SKILL.md の「Semantic クラスの種類」セクションに composite 型項目追加 (TODO) |
Pilot 3 で更新したファイル
be/src/Module/AppModule.php (実体は src/Module/AppModule.php):
- Pilot 3 用 3 件の bind 追加 (
OrderQueryInterfaceSingleton,PurchaseFlowInterface,PaymentMethodFactoryInterface)
Pilot 3 で新規追加した Reason / Semantic 群:
Reason/Entity/:OrderEntity,PurchaseTotals,PaymentVerificationReason/Query/:OrderQueryInterface,FakeOrderQueryReason/Service/:PaymentMethodFactoryInterface,FakePaymentMethodFactory,PaymentMethodInterface,PurchaseFlowInterface,FakePurchaseFlowReason/:PaymentSuccessCase,PaymentFailureCase(branching discriminator)Semantic/: 14 件 (上記指標 #5)Exception/: 14 件 (Semantic format 系 12 +PreOrderNotFoundException)
Pilot 3 の参照ドキュメント
be/docs/be-adoption-evaluation.md§6 — Cascade Diamond 不成立の機構詳細 (Ray.Di の#[Input]非対応、BecomingArguments::be()のget_object_vars()経路、Linear Cascade への縮退ルール)be/docs/be-adoption-evaluation.md §5— パターン採用判定表 (Branching ◎採用 / Cascade Diamond ✗構造的に不成立)~/git/be-patterns/demos/medical-triage/— Branching 参照実装 (Pilot 3 の typed discriminator 設計はここから流用)
Pilot 1+2+3 を通じた集計
- composer test: 27/27 pass, 79 assertions, 0 notices
- Be domain LoC (Pilot 1+2+3 累計): 約 1670 src + 約 514 tests (Pilot 1 = 576 + 20、Pilot 2 = 620 + 250、Pilot 3 = 469 + 132、新規 Semantic / Reason / Exception 共有部分は重複カウントなし)
- 採用パターン: Linear/Minimal (Pilot 1), Cascade (Pilot 2), Linear Cascade + Branching (Pilot 3)
- 未検証パターン: Multi-Reason Being, Complex Convergence (
insurance-claim系), 真の Cascade Diamond (apex が Input 不要なケース; EC-CUBE の通常 transition では出現しない可能性大)
Pilot 4 — doRegisterCustomer (Multi-Reason Being)
目的: Multi-Reason Being パターン (be-patterns blog-publishing 系) の参照実装。1 つの Being が複数の独立な #[Inject] Reason を持ち、互いに依存しない server-derived scalar を並列生成する構造の動作確認。
| 項目 | 値 |
|---|---|
| リポジトリ | ~/git/BeMart |
| パターン | Multi-Reason Being (1 段) + Final (永続化)。RegisterCustomerInput → CustomerRegistering (Being: 4 つの独立 Reason 並列起動) → CustomerRegistered (Final: 永続化のみ)。Being は (1) EmailUniquenessCheckerInterface (uniqueness fail-fast), (2) CustomerIdQueryInterface (32-char opaque hex), (3) PasswordHasherInterface (bcrypt), (4) CustomerInitialPointInterface (welcome bonus) を #[Inject] で並列に呼ぶ。各 Reason の結果 (customerId / passwordHash / initialPoint / customerStatus=2 固定) は Being 自身の readonly プロパティに格納され、#[Input] 経由で Final に forward |
| テスト | 39 passed (Pilot 1 既存 8 + Pilot 2 既存 13 + Pilot 3 既存 6 + Pilot 4 新規 12), 111 assertions, 0 notices (Pilot 4 で 19 件の Semantic 変数を新規登録: client-input 15 件 Email / Password / Name01 / Name02 / Kana01 / Kana02 / CompanyName / PhoneNumber / PostalCode / Pref / Addr01 / Addr02 / Birth / Sex / Job + server-derived 4 件 CustomerId / PasswordHash / InitialPoint / CustomerStatus を MergedCart パターン (空 #[Validate] body) で登録) |
| Skill 配置 | ~/.claude/skills/alps-to-be-bear/ (Multi-Reason Being テンプレ追加が必要) |
| スコープ決定 | email 検証 OFF 経路のみ実装 (customerStatus = 2 固定)。検証 ON (provisional → email confirm → activate) は将来の Branching pilot で実装。理由: Branching 機構自体は Pilot 3 で検証済みのため、ここで再検証しても新たな知見は得られない |
改訂履歴 (2026-05-18):
- ALPS 分析 →
doRegisterCustomer(4 必須フィールド) +CustomerRegistrationcontainer (11 オプショナル) を Input にマップ。CustomerRegistrationComplete.descriptor=[#goTop]のみ →#[Link(rel: 'goTop', ...)]1 件 - パターン判定: 4 つの Reason が互いに独立で並列実行可能 → Multi-Reason Being (blog-publishing 準拠)。Diamond ではない (各 Reason の結果が他の Reason に流れない)
- domain-review subagent → pass (findings 7 / blocking 0)。指摘で実装に反映: (a)
InitialPoint/CustomerStatusの docblock 主張をバリデータと整合させる, (b)FakeCustomerStorage::getByEmail()を追加してテストの Reflection を除去 - application-review subagent → pass (findings 0 / blocking 0)
- security-review subagent → pass (findings 7 / blocking 0)。指摘で実装に反映:
CustomerRegistering::__constructの$passwordをpublicから外し#[SensitiveParameter]付与。plaintext が Being の public surface に露出せず、stack trace にも redact される
Pilot 4 完了の判定基準 — 12 指標
| # | 指標 | Target | Actual | Note |
|---|---|---|---|---|
| 1 | composer test 全 pass | 100% | 100% | 39/39 pass (Pilot 1+2+3 既存 27 + Pilot 4 Domain 7 + Resource 5) |
| 2 | 意味ログ被覆 | 100% | 100% | DevBecoming が Input → Being → Final 全体を var/log/bemart.json に自動記録 |
| 3 | i18n 例外メッセージ | 100% | 100% | 全 DomainException (EmailAlreadyRegisteredException + Semantic format 系 15 件) に #[Message(['en'=>..., 'ja'=>...])] |
| 4 | 自己証明 assert | ≥1 | ≥1 | Resource/Page/Entry.php:78 で assert($final instanceof CustomerRegistered)。Being 内では EmailUniquenessCheckerInterface::ensureUnique() が「重複なし」を自己証明 (例外で否定証明) |
| 5 | Semantic クラス数 | 19 | 19 | client-input 15 (Email / Password / Name01 / Name02 / Kana01 / Kana02 / CompanyName / PhoneNumber / PostalCode / Pref / Addr01 / Addr02 / Birth / Sex / Job) + server-derived 4 (CustomerId / PasswordHash / InitialPoint / CustomerStatus)。server-derived は空 #[Validate] body (composite Semantic と同じ「型断定のみ。値の契約は Service」パターン) |
| 6 | Reason 層共有ストア | Singleton 必須 | 該当 | FakeCustomerStorage を Scope::SINGLETON で bind (AppModule:80)。FakeCustomerCommand (write) と FakeEmailUniquenessChecker (read) が同一 storage を参照することで、Command の書き込みが同一 request 内で uniqueness check に見える |
| 7 | client-input / server-fetched 分離 | 2 シート | 2 シート | client-input (15 フィールド: email / password / name01 / name02 / 11 オプショナル) と server-derived (customerId / passwordHash / initialPoint / customerStatus) を Being 内で分離。Being の public surface に両方並ぶが、Final に流れる際は passwordHash のみ (plaintext password は #[SensitiveParameter] で promoted から外し、Being の public プロパティから除外) |
| 8 | LoC (Pilot 4 新規分) | 実測のみ | src 約 540 + tests 約 200 | Input 1 件 (54 LoC) + Being 1 件 (87 LoC) + Final 1 件 (97 LoC) + Resource 1 件 (112 LoC) + Semantic 19 件 (約 250 LoC 合計) + Exception 16 件 (約 200 LoC 合計) + Reason Entity 1 件 (40 LoC) + Reason Query/Service 12 件 (約 250 LoC 合計) + Test 200 LoC |
| 9 | Multi-Reason Being テスト | pass | pass | testHappyPathPersistsAndReturnsServerScalars で Being の 4 つの Reason 並列実行 (uniqueness OK + id生成 + hash + point) を統合検証 |
| 10 | 重複 email rejection | pass | pass | testDuplicateEmailIsRejected (Domain) / testOnPostDuplicateEmailReturns409 (Resource) で EmailAlreadyRegisteredException → HTTP 409 マッピングを検証。alice@example.com は seed fixture に存在 |
| 11 | password hash 非露出 + 永続側 round-trip | pass | pass | testPasswordIsHashedAndNotExposed: property_exists(CustomerRegistered::class, 'passwordHash') === false で Final の surface に hash 不在を確認。password_verify($plain, $persisted->passwordHash) で永続側の round-trip も検証 |
| 12 | ALPS 整合性 (Rule 7) | 手動チェック合格 | 合格 | Final (CustomerRegistered) の public プロパティ (customerId / email / name01 / name02 / initialPoint / customerStatus) は CustomerRegistrationComplete container の状態と整合。Resource の #[Link(rel: 'goTop')] は CustomerRegistrationComplete.descriptor = [#goTop] に完全一致 |
Pilot 4 で発見された skill gap
| ID | 内容 | 昇格先 |
|---|---|---|
| G-11 | Multi-Reason Being の構造的特徴 — Diamond と区別するための判定基準: 「各 Reason の結果が他の Reason の入力にならず、互いに独立に並列実行できる」場合は Multi-Reason Being (be-patterns blog-publishing)、結果が他の Reason に流れる場合は Diamond/Cascade。Pilot 4 では (uniqueness check / id 生成 / password hash / initial point) が完全独立 | SKILL.md の「パターン判定フロー」に Multi-Reason Being の判定基準セクション追加 (TODO) |
| G-12 | Multi-Reason Being では Reason の種類が混在しても良い — Pilot 4 では「fail-fast query」(EmailUniquenessChecker) + 「pure derivation」(Hash / IdGen / PointService) が同じ Being に同居。blog-publishing の元パターンは pure derivation のみだが、guard を 1 つ混ぜても Diamond にはならない | SKILL.md の Multi-Reason Being テンプレに「guard + pure derivation の混在を許容」明記 (TODO) |
| G-13 | plaintext password の #[SensitiveParameter] + 非 public 化 — 暗号化されるべき入力は Being の __construct パラメータで受け取り内部で hash 化、public promoted property にしない (#[Input] #[SensitiveParameter] string $password の形)。be-framework の cascade は public プロパティだけを下流に流すので、非 public にすれば自動的に Final に到達しない |
SKILL.md の「機密データ取り扱い」セクション新規追加 (TODO) |
Pilot 4 で更新したファイル
src/Module/AppModule.php:
- Pilot 4 用 6 件の bind 追加 (
FakeCustomerStorageSingleton,EmailUniquenessCheckerInterface,CustomerCommandInterface,PasswordHasherInterface,CustomerIdQueryInterface,CustomerInitialPointInterface)
Pilot 4 で新規追加した Be 層 (be/src/):
Input/:RegisterCustomerInput.phpBeing/:CustomerRegistering.phpFinal/:CustomerRegistered.phpReason/Entity/:CustomerEntity.phpReason/Query/:CustomerCommandInterface.php,EmailUniquenessCheckerInterface.php,FakeCustomerStorage.php(Singleton),FakeCustomerCommand.php,FakeEmailUniquenessChecker.phpReason/Service/:PasswordHasherInterface.php+NativePasswordHasher.php,CustomerIdQueryInterface.php+FakeCustomerIdProvider.php,CustomerInitialPointInterface.php+FakeCustomerInitialPoint.phpSemantic/: 19 件 (指標 #5)Exception/: 16 件 (15 FormatException +EmailAlreadyRegisteredException)var/fake/customers.json— 3 件の seed (alice / bob / carol。passwordHash は文法上 valid だがpassword_verifyを通らないダミー文字列。$commentでダミーである旨を明記)
Pilot 4 で新規追加した BEAR 層:
src/Resource/Page/Entry.php—page://self/entryのonPost(15 引数: 4 required + 11 nullable)tests/Resource/EntryResourceTest.php— 5 tests (happy / optional fields / 409 / 400 invalid email / 400 empty password)be/tests/Domain/CustomerRegisteredTest.php— 7 tests (happy / optional fields / hash 非露出 + round-trip / duplicate / invalid email / empty password / empty name01)
Pilot 4 の security 観点 (security-review findings から記録)
- Pilot スコープでの既知トレードオフ:
- email 検証 OFF 即 Active (
customerStatus = 2) → アドレス squat 可能 (将来の Branching pilot で検証 ON 経路実装で解消) - 重複 email 409 + body
"The email is already registered."は user enumeration oracle。検証 ON 経路では「silent 成功 + メール通知で重複を伝える」設計に切替予定 AppModuleがFakeCustomerStorageを無条件 bind。本番デプロイ前に env-gatedProdModule切替が必要 (Pilot 1-3 と同根の Phase B 案件)
- email 検証 OFF 即 Active (
- Pilot 4 で実装した security guard:
- plaintext password は Being の non-public parameter +
#[SensitiveParameter]→ stack trace redact + 下流 public surface 不在 CustomerIdはbin2hex(random_bytes(16))(128-bit CSPRNG opaque id) で EC-CUBE の sequential bigint と違って予測不能- Final (
CustomerRegistered) の public surface は self-registration projection のみ (customerId / email / name01 / name02 / initialPoint / customerStatus)。passwordHashおよび 11 のオプショナル PII (kana / 住所 / 電話 / 生年月日 / 性別 / 職業 / 会社名) は永続側にのみ保持し、レスポンス body には含めない
- plaintext password は Being の non-public parameter +
- Phase B (Pilot 終了後) に着手すべき項目: CSRF / rate-limit /
bear/security+ Psalm taint / env-gatedProdModule/ 検証 ON 経路 Branching
Pilot 4 の参照ドキュメント
~/git/be-patterns/demos/blog-publishing/— Multi-Reason Being の正解パターン (Pilot 4 設計の参照元)be/docs/be-adoption-evaluation.md— Pilot 1+2+3 の採用評価 (Pilot 4 で Multi-Reason Being を採用パターンに追加予定; TODO)
Pilot 1+2+3+4 を通じた集計
- composer test: 39/39 pass, 111 assertions, 0 notices
- Be domain LoC (累計): 約 2210 src + 約 714 tests
- 採用パターン: Linear/Minimal (Pilot 1), Cascade (Pilot 2), Linear Cascade + Branching (Pilot 3), Multi-Reason Being (Pilot 4)
- 未検証パターン: Complex Convergence (
insurance-claim系), 真の Cascade Diamond (apex が Input 不要なケース; EC-CUBE の通常 transition では出現しない可能性大)
Pilot 5 — doCheckout (Complex Convergence / Multi-side-effect Final)
目的: 「Final で複数の独立 side-effect Reason を 1 constructor 内で収束させる」初回ピロット。Pilot 4 までは Final が単一 CustomerCommand のみ呼ぶ単純な形だったが、doCheckout は OrderCommand.register + Mailer.sendOrderConfirmation + CartCommand.clearByPreOrderId の 3 副作用を Final で並走させる。これが be-patterns loan-application で言う Complex Convergence の本質。
| 項目 | 値 |
|---|---|
| リポジトリ | ~/git/BeMart |
| パターン | Diamond-Cascade (2 段 Being + Multi-side-effect Final)。CheckoutInput → CheckoutPrepared (Being: pre-order 取得 + 金額確定) → CheckoutSettled (Being: 在庫引当 → 決済 → 注文番号発番、3 Reason の strict sequence) → CheckoutCompleted (Final: 注文永続化 + メール送信 + カートクリアの 3 副作用収束)。失敗時は Branching Final を使わず Reason 内 DomainException → Resource 層 HTTP code マッピング (Pilot 3 で Branching は検証済みのため重複を避けた) |
| テスト | 52 passed (Pilot 1-4 既存 39 + Pilot 5 新規 13), 149 assertions, 0 notices (Pilot 5 で server-derived Semantic 3 件 OrderNo / OrderDate / PaymentDate を MergedCart パターン (空 #[Validate] body) で追加。client-input 側は Pilot 3 の PreOrderId / PaymentMethodId を再利用) |
| Skill 配置 | ~/.claude/skills/alps-to-be-bear/ (Multi-side-effect Final テンプレ + Ray.Di toInstance 注意書きの追加が必要) |
| スコープ決定 | happy-path + 失敗時 422/404 のみ実装。Branching Final (CheckoutFailed) と補償処理 (Refund / InventoryRelease) は Phase B の決済セキュリティピロットへ deferred。理由: Branching 機構自体は Pilot 3 で検証済み。Pilot 5 の新規性は「Multi-side-effect Final の収束」に絞る |
改訂履歴 (2026-05-18):
- ALPS 分析 →
doCheckout(paymentMethodclient-input) +preOrder既存 →CheckoutInput(preOrderId, paymentMethodId)にマップ。ShoppingComplete.descriptorから#[Link(rel: 'goTop')]+#[Link(rel: 'goCart')]を導出 - パターン判定: 3 Reason が strict sequence (inventory before payment before number-gen) → Diamond-Cascade (loan-application 準拠)。Pilot 4 の Multi-Reason Being (並列 Reason) と区別: Pilot 5 は順序依存 + Final で複数副作用
- Ray.Di binding gotcha 発見 (G-14):
bind(Iface)->to(Impl)はbind(Impl)->in(SINGLETON)を consult しない。Iface の linked binding は singleton scope と独立に新 Impl を生成する。test 5 件中 2 件 (mailer/gateway captures) が初期実装で fail。toInstance($obj)パターンで両 binding を同一 object reference に固定 → 解決 - domain-review subagent → pass (findings 5 / blocking 0)。指摘は全て non-blocking style nit (Final public surface が ALPS ShoppingComplete より広い / DateTimeImmutable 直生成 / FinalizedOrderEntity inline 生成 / FakeInventoryAllocator atomicity コメント / AppModule コメント評価)
- application-review subagent → pass (findings 6 / blocking 0)。指摘は全て non-blocking (Code::UNPROCESSABLE_ENTITY 欠如の literal 422 / Location ヘッダ test 強化 / locale ‘ja’ ハードコード / paymentMethodId 不正系 400 test / 4xx で orderNo 漏れていない assert /
@varPHPDoc 追加) - security-review subagent → pass (findings 13 / blocking 0)。重要な Phase B 課題を多数発見 (詳細は下記)
Pilot 5 完了の判定基準 — 13 指標
| # | 指標 | Target | Actual | Note |
|---|---|---|---|---|
| 1 | composer test 全 pass | 100% | 100% | 52/52 pass (Pilot 1-4 既存 39 + Pilot 5 Domain 8 + Resource 5) |
| 2 | 意味ログ被覆 | 100% | 100% | DevBecoming が CheckoutInput → CheckoutPrepared → CheckoutSettled → CheckoutCompleted 全体を var/log/bemart.json に自動記録 |
| 3 | i18n 例外メッセージ | 100% | 100% | 全 DomainException (InsufficientStockException / PaymentDeclinedException 新規 + PreOrderNotFoundException Pilot 3 既存) に #[Message(['en'=>..., 'ja'=>...])] |
| 4 | 自己証明 assert | ≥1 | ≥1 | Resource/Page/Shopping/Checkout.php で assert($final instanceof CheckoutCompleted)。CheckoutSettled の existence proof は「stock allocated AND payment captured AND order number issued」、CheckoutCompleted の existence proof は「persisted AND mail sent AND cart cleared」 |
| 5 | Semantic クラス数 | 0 (新規) | 0 | Pilot 3 の PreOrderId / PaymentMethodId を共有。重複作成を避けた |
| 6 | Reason 層共有ストア | toInstance 必須 | 該当 | FakeInventoryAllocator / FakePaymentGateway / FakeMailer を toInstance($obj) で Iface + Impl の両 binding に固定 (AppModule:121-129)。FakeFinalizedOrderStorage は Becoming chain から直接見られないため通常の in(SINGLETON) で十分 (Pilot 4 の FakeCustomerStorage と同パターン) |
| 7 | client-input / server-fetched / side-effect 分離 | 3 シート | 3 シート | client-input (preOrderId / paymentMethodId), server-fetched (OrderEntity from OrderQuery, PurchaseTotals totals from PurchaseFlow), side-effect (InventoryAllocator / PaymentGateway / OrderNoProvider / OrderCommand / Mailer / CartCommand) を 3 段階に明確分離 |
| 8 | LoC (Pilot 5 新規分) | 実測のみ | src 約 670 + tests 約 240 | Input 1 件 (29 LoC) + Being 2 件 (71 + 71 LoC) + Final 1 件 (98 LoC) + Resource 1 件 (95 LoC) + Exception 2 件 (約 40 LoC) + Reason Entity 1 件 (61 LoC) + Reason Service Iface 4 + Fake 4 (約 260 LoC) + Reason Query Iface 1 + Fake 2 (約 110 LoC) + Test Domain 154 LoC + Resource 87 LoC |
| 9 | Multi-side-effect Final テスト | pass | pass | testPersistsFinalizedOrder + testSendsExactlyOneConfirmationMail + testCapturesPaymentExactlyOnceWithCorrectAmount + testClearsSourceCart で 4 副作用 (persist / mail / payment capture / cart clear) を独立検証。Final の 3 Reason すべてが期待回数だけ呼ばれたことを保証 |
| 10 | DomainException → HTTP 422/404 マッピング | pass | pass | testUnknownPreOrderRejected (Domain PreOrderNotFoundException) + testOnPostUnknownPreOrderReturns404 (Resource 404), testInsufficientStockRejected (Domain) + testOnPostInsufficientStockReturns422 (Resource 422), testPaymentDeclinedRejected (Domain) + testOnPostPaymentDeclinedReturns422 (Resource 422) で 3 失敗経路を Domain + Resource 両層で検証 |
| 11 | side-effect strict sequence | pass | pass | CheckoutSettled で inventory.allocate → gateway.checkout → numbers.generate の順 (在庫を確保してから決済、無駄な番号発番をしない)。CheckoutCompleted で orderCommand.register → mailer.send → cartCommand.clear の順 (record of truth が先、cart cleanup は最後)。testInsufficientStockRejected は stock 不足時に gateway が呼ばれないことを captures count で検証可能 |
| 12 | ALPS 整合性 (Rule 7) | 手動チェック合格 | 概ね合格 (1 件 non-blocking finding) | Final (CheckoutCompleted) の public プロパティ (orderNo / completeMessage / customerId / total / paymentTotal / addPoint / orderStatus / orderDate / paymentDate) は ShoppingComplete 表示状態 + audit info として整合。domain-review で「ShoppingComplete descriptor は orderNo / completeMessage のみ。残り 7 件は EC-CUBE Order 表示に必要な audit info — Resource 層で projection 縮小を検討」の finding を non-blocking 扱い |
| 13 | Ray.Di binding correctness | pass | pass | 上記指標 #6。Pilot 5 で発見した toInstance パターンを AppModule のコメント (lines 102-119) に詳述。今後の Pilot は Iface + Impl 両参照を要する Fake で同パターンを踏襲 |
Pilot 5 で発見された skill gap
| ID | 内容 | 昇格先 |
|---|---|---|
| G-14 | Ray.Di bind(Iface)->to(Impl) は bind(Impl)->in(SINGLETON) を consult しない — Iface 経由の resolution と Impl 経由の resolution が独立した instance を作る。Pilot 5 の FakeMailer / FakePaymentGateway (state を hold する Fake) は Becoming chain が MailerInterface で resolve、test introspection が FakeMailer で resolve するため、両 binding が同一 instance を返す必要がある。Solution: $obj = new Fake(); $this->bind(Iface)->toInstance($obj); $this->bind(Impl)->toInstance($obj); (Storage 経由で state を分離するパターンも可だが、refactor 量が大きい) |
SKILL.md の「Ray.Di binding patterns for Fakes」セクション新規追加 (TODO)。Pilot 1-4 で使ってきた bind(Iface)->to(Impl); bind(Impl)->in(SINGLETON) パターンは「Impl が state-less で test が直接参照しない」場合に限る旨を明記 |
| G-15 | Multi-side-effect Final (Complex Convergence) の判定基準 — Pilot 4 までの Final は単一 Command のみ呼んだが、Pilot 5 の Final は OrderCommand + Mailer + CartCommand の 3 副作用を 1 constructor で並走させる。判定基準: 「副作用同士に順序依存があり (record of truth が先 / cleanup が後)、互いの結果に依存しない」場合は Multi-side-effect Final として 1 つの Final で収束させて良い。3 副作用以上で各副作用が他の副作用の結果を要する場合は Cascade Final (中間 Final を挟む) を検討 | SKILL.md の「パターン判定フロー」に Multi-side-effect Final の項目追加 (TODO) |
| G-16 | Failure mode: side-effect ordering と partial-commit window — Pilot 5 で gateway.checkout() が成功した後に numbers.get() が throw (現状の Fake では起きないが Phase 2 で起きうる) すると、顧客は課金されたが orderNo 未発番 → FinalizedOrder 未永続化 → カートも残る状態に陥る。同様に Final で orderCommand.register() が成功して mailer.send() が throw すると永続化 + 課金完了 + メール無し + カート残存。Solution (Phase B): (a) Final の Mailer は契約上 non-throwing (失敗時は internal log + swallow)、(b) CartCommand 失敗も swallow (注文は durable なので stale cart は許容)、(c) CheckoutSettled は Phase 2 で DB transaction + register_shutdown_function gateway hook に書き換え |
SKILL.md の「side-effect ordering と例外契約」セクション (TODO) |
Pilot 5 で更新したファイル
src/Module/AppModule.php:
- Pilot 5 用 9 件の bind 追加:
FakeFinalizedOrderStorageSingleton,FakeInventoryAllocator/FakePaymentGateway/FakeMailerの toInstance による Iface + Impl 両 binding,OrderNoProvider/OrderCommandInterfaceの通常 link binding - 11-19 行のコメントブロックで Ray.Di toInstance パターンを文書化 (将来 pilot 用の breadcrumb)
Pilot 5 で新規追加した Be 層 (be/src/):
Input/:CheckoutInput.phpBeing/:CheckoutPrepared.php,CheckoutSettled.phpFinal/:CheckoutCompleted.phpException/:InsufficientStockException.php,PaymentDeclinedException.php(PreOrderNotFoundExceptionは Pilot 3 既存)Reason/Entity/:FinalizedOrderEntity.php(16 fields + STATUS_NEW=1 constant)Reason/Service/:InventoryAllocatorInterface.php+FakeInventoryAllocator.php(inventory.json 読み + atomic 引当),PaymentGatewayInterface.php+FakePaymentGateway.php(paymentMethodId===9 で決済失敗シミュレーション),OrderNoProvider.php+OrderNoProvider.php(bin2hex(random_bytes(16))で 32-hex),MailerInterface.php+FakeMailer.php(sent 配列で送信記録 / non-throwing 契約)Reason/Query/:OrderCommandInterface.php+FakeOrderCommand.php(FakeFinalizedOrderStorage delegate),FakeFinalizedOrderStorage.php(Singleton, orderNo + preOrderId 両ルックアップ)Reason/Query/CartCommandInterface.php— 既存にclearByPreOrderId(string $preOrderId): void追加Reason/Query/FakeCartCommand.php— 同上 implementationReason/Query/FakeCartStorage.php—removeByPreOrderId()/getByPreOrderId()追加var/fake/orders.json— Pilot 5 用 3 件 pre-order 追加:aaaa…aaaa(happy, paymentMethodId=2, sample-001×1@1500, delivery=600 → total 2250),bbbb…bbbb(OOS, oos-pilot5-tightstock×3 だが inventory=1),cccc…cccc(declined, paymentMethodId=9)var/fake/carts.json— Pilot 5 happy-path 用session-checkout-pilot5cart 追加 (clearByPreOrderId テストのため)var/fake/inventory.json— 新規。{sample-001: 10, preorder-2026-spring-bag: 5, oos-pilot5-tightstock: 1}docs/pilot5/alps-analyze.md— 新規 (Pilot 5 設計の handover ドキュメント)
Pilot 5 で新規追加した BEAR 層:
src/Resource/Page/Shopping/Checkout.php—page://self/shopping/checkoutのonPost(string $preOrderId, int $paymentMethodId)(4 例外を 400/404/422/422 にマップ + success 9 fields projection)tests/Resource/CheckoutResourceTest.php— 5 tests (happy 201 / 404 unknown / 422 OOS / 422 declined / 400 malformed)be/tests/Domain/CheckoutCompletedTest.php— 8 tests (happy / persist / mail送信 1 回 / payment capture 1 回 + amount=2250 / cart clear / 404 unknown / 422 OOS / 422 declined)
Pilot 5 の security 観点 (security-review findings から記録)
Phase B / Phase 2 に着手すべき項目 (security-review 13 finding の要約):
- AUTHZ 欠如: Resource は preOrderId 所有者 (customerId) を session と照合していない。誰でも他人の pre-order を確定できる。Phase B:
CheckoutPreparedにSessionGuardReason 追加 →OrderEntity.customerId !== session.customerIdでUnauthorizedPreOrderAccessExceptionthrow - MASS-ASSIGNMENT: client supplied
paymentMethodIdをOrderEntity.paymentMethodIdと照合せず gateway に forward。決済方法のすり替えが可能。Phase B fix:CheckoutInputからpaymentMethodIdを削除し、CheckoutPreparedが$order->paymentMethodIdを採用する形に変更 - IDEMPOTENCY: pre-order row を「消費済み」状態に遷移させていない。同じ preOrderId の double-submit で 二重課金 + 二重 FinalizedOrder + 在庫二重引当 が発生しうる。現状唯一の緩和策は最後の
clearByPreOrderIdだが redirect を follow しない並列 / replay 攻撃には無力。Phase B fix: 実 DB ではOrderCommand.register()をSELECT FOR UPDATEまたはdtb_order.pre_order_idUNIQUE 制約 +INSERT ON CONFLICTで atomic にする - PARTIAL-COMMIT WINDOW: gateway.checkout 成功後の throw / orderCommand.register 後の mailer throw / cartCommand throw で各種の partial state が発生しうる。Phase B fix: Mailer / CartCommand を契約上 non-throwing にして Final 側で try/catch + log + swallow。CheckoutSettled は Phase 2 で DB transaction +
register_shutdown_function化 - PAYMENT-FIRST ORDERING 正しい: inventory.allocate → gateway.checkout の順を守っているため OOS 時に課金しない設計は OK (no action required)
- PRE-ORDER ID ENTROPY (cross-pilot): Pilot 5 では mint しないが、将来の
doShoppingpilot で実装するPreOrderIdProviderは 必ずbin2hex(random_bytes(20))CSPRNG にすること (上記 AUTHZ 欠如と組み合わさると enumeration 攻撃が成立する) - EXCEPTION MESSAGE 漏洩:
InsufficientStockException(productCode + counts),PaymentDeclinedException(preOrderId + paymentMethodId + amount) が SemanticLogger 経由でログに残る。Resource は固定文字列を返すので HTTP body には漏れない。Phase B fix: preOrderId はログでは前 8 桁に redact、または sensitive channel に分離 - RESPONSE BODY 漏洩: success body に
customerId(32-hex opaque) が含まれる。session で既知のため client 側で必要ない。Phase B fix: response body からcustomerIdを drop - INPUT VALIDATION 完全:
onPost(string $preOrderId, int $paymentMethodId)以外の body フィールドは BEAR が signature reject。over-posting 不可 - CSRF / REPLAY: Pilot 5 は CSRF guard 無し。Phase B で標準 CSRF middleware を投入予定。Pilot 5 は CSRF retrofit を阻害しない設計 (no GET fallback / no JSONP)
- AppModule toInstance パターン (Phase 2): 本番 PaymentGateway / Mailer は stateless (delegate to external) のため
bind(Iface)->to(Impl)->in(SINGLETON)で十分。ただし state を hold する production wrapper (例: idempotency cache, connection pool, metrics counter) を追加する場合は Pilot 5 の toInstance パターンに準じる必要あり - SemanticLogger 機密漏洩 (Phase B): DevSemanticLogger が CheckoutInput / OrderEntity (customerId + items + prices) / PurchaseTotals / FinalizedOrderEntity を平文で
var/log/bemart.jsonに書く。Phase B fix: ProdModule で per-class allowlist + redacted logger に切替。Card PAN / CVV は Be 層に到達しない設計 (PaymentGatewayInterface は paymentMethodId のみ受け取る) なので PCI 対象は order metadata に限定される - FakePaymentGateway 規約: paymentMethodId===9 を「決済失敗」マジックナンバーに使用。fixture コメントに記録済み。Phase 2 ProdModule は同マジックナンバーを継承しないこと
Pilot 5 の参照ドキュメント
~/git/be-patterns/demos/loan-application/— Diamond-Cascade パターンの正解 (Pilot 5 設計の参照元)be/docs/pilot5/alps-analyze.md— Pilot 5 の設計 handover (descriptor → pattern → reasons → BEAR mapping)HANDOVER.mdPilot 4 の skill gap G-11/G-12/G-13 — Multi-Reason Being との混同を避けるための参考
Pilot 1+2+3+4+5 を通じた集計
- composer test: 52/52 pass, 149 assertions, 0 notices (Pilot 5 follow-up commit
3ad9821で server-derived Semantic 3 件OrderNo/OrderDate/PaymentDateを MergedCart パターンで追加) - Be domain LoC (累計): 約 2880 src + 約 954 tests
- 採用パターン: Linear/Minimal (Pilot 1), Cascade (Pilot 2), Linear Cascade + Branching (Pilot 3), Multi-Reason Being (Pilot 4), Diamond-Cascade + Multi-side-effect Final (Pilot 5)
- 未検証パターン: 真の Cascade Diamond (apex が Input 不要なケース; EC-CUBE の通常 transition では出現しない可能性大), Complex Convergence の更に深いネスト (insurance-claim の Multi-stage Multi-Reason)
- Phase A 完了 (Pilot 1-5)。Phase B は Psalm taint 設定から着手 (下記参照)
Phase B — Security / Hardening Slice 1: Psalm Setup
目的: 静的解析を Phase A コードに後付けで導入し、Phase B 以降の判定基盤を整える。決定的要素側に置ける security チェック を 1 つ確立する。
完了した作業
- Psalm 6.16 を
composer require --devで導入 (vimeo/psalm) psalm.xmlを errorLevel=3 + 主要な MixedX / Unused 系を suppress で起動 (Pilot 1-5 コードに後付けで適用するため)- baseline 生成:
psalm-baseline.xmlに既存 11 件をベースライン化 (MissingOverrideAttribute9 件 +InvalidOperand2 件 inFakePurchaseFlowの tax/point 計算 int/float 混合) - composer scripts 追加:
composer psalm— 通常解析 (baseline 適用、exit 0)composer psalm-taint— taint 解析 (--ignore-baselineでモード切替時のUnusedBaselineEntryを抑制、exit 0)
Slice 1 の現状認識 (重要)
composer psalm-taint は exit 0 だが、これは「Phase A コードベースで Psalm の標準 taint mechanism が検出する flow が存在しない」ことを意味する。「PII flow が無い」ことではない。
Phase A の構造的事実:
- DB クエリ: 全部 Fake (in-memory) → SQL sink が存在しない
- shell exec: なし
- 明示的な
log()呼び出し: なし (SemanticLogger は framework 内で自動) - HTML/string concat: JSON response のみ
- Psalm が標準で知る source:
$_GET/$_POST/$_REQUEST等の superglobal。BEAR の#[Input]や Be の Input クラスは未知
→ Psalm 標準では BEAR/Be の HTTP user input → SemanticLogger 平文出力 の flow を捕捉できない。security-review が指摘した PII 漏洩 (customerId / items / prices が var/log/bemart.json に平文出力) は taint analysis では検出されない。
Slice 2 候補 (Psalm taint を BEAR/Be に対応させる)
未実施。実施候補:
- オプション A: BEAR Resource の
onGet/onPostparameters に@psalm-taint-sourceannotation を手動追加 (workflow prompt で自動化) - オプション B:
SemanticLogger::log()系の write API に@psalm-taint-sinkannotation - オプション C: Psalm plugin で
#[Input]attribute を taint source として自動認識 (be-framework 側に PR を出す筋)
判断: Slice 1 では Phase B の 足場を作る ことに集中。アノテーション戦略は Phase B Slice 2 で別途決定。
Slice 2 以降の Phase B 項目 (security-review の累積 finding から)
未着手:
- CSRF token — 全 onPost 系 Resource に適用 (Pilot 3-5 の 3 endpoint が対象)
- rate-limit —
/shopping/checkout等の sensitive endpoint - bear-security-setup skill の適用 — 認証 / 認可基盤
- env-gated ProdModule —
SemanticLoggerを prod で per-class allowlist + redacted 出力に切替 - AUTHZ check —
CheckoutPreparedのSessionGuardReason (Pilot 5 security-review F-1) - mass-assignment fix —
CheckoutInput::paymentMethodIdを削除しOrderEntity::paymentMethodIdを採用 (Pilot 5 F-2) - idempotency —
dtb_order.pre_order_idUNIQUE 制約 + 実 DB でSELECT FOR UPDATE(Pilot 5 F-3)
Slice 1 の振り返り (決定的/非決定的)
- 決定的側で確立できたもの: Psalm の存在 / baseline / composer 起動口。これらは「一度作れば後続に効く」道具立て
- 非決定的側で残ったもの: BEAR/Be の taint source/sink 戦略 (3 案あり、業務的に何を「機密」と扱うかの判断と絡む)。Phase B Slice 2 でユーザー判断を仰ぐ
Phase B — Slice 3: env-gated ProdModule (DevBecoming PII leak fix)
目的: Pilot 5 security-review F-12 (DevSemanticLogger が var/log/bemart.json に customerId / 明細 / 価格 / paymentTotal を平文出力) を、prod context で 構造的に 塞ぐ。AppModule(dev default) は触らず、ProdModule で override する形を取る。
完了した作業
src/Module/ProdLoggingOverrideModule.php新規 —BecomingInterfaceを DevBecoming wrapper なし で素のBe\Framework\Becomingに rebind。SemanticLoggerInterfaceもDevSemanticLoggerではなく素のKoriym\SemanticLogger\SemanticLoggerに rebind。In-memory の semantic trace は引き続き framework が収集するが、ファイルには永続化されないsrc/Module/ProdModule.php新規 —AbstractAppModuleを継承し、AppModuleを install した上でProdLoggingOverrideModuleで override。エントリポイント (将来のbin/app.php/public/index.php) はAPP_CONTEXT=prodで ProdModule を選ぶ想定だが、現状の Pilot は CLI smoke (bin/smoke_phase6.php) のみのため env switching の bin 改修は deferredtests/Module/ProdModuleTest.php新規 — 4 ケース:- ProdModule context で
BecomingInterfaceを resolve →Becoming::class(notDevBecoming) - ProdModule context で
SemanticLoggerInterfaceを resolve →SemanticLogger::class(notDevSemanticLogger) - ProdModule context で実際に
/shopping/checkoutを叩いて 201 を返した後、var/log/bemart.jsonが 存在しない ことを assert - 負の対照: AppModule (dev) で同じリクエストを叩くと
var/log/bemart.jsonが 存在する ことを assert (この対照が壊れたら #3 が vacuous になるので必須)
- ProdModule context で
確認結果
composer test: 56/56 pass, 154 assertions, 0 notices (Phase A 既存 52 + Phase B Slice 3 新規 4)composer psalm: exit 0 (baseline 11 件のまま)composer psalm-taint: exit 0
Slice 3 の振り返り (決定的/非決定的)
- 決定的側: 「prod context は
BecomingInterfaceをBecomingに直結しDevBecomingを挟まない」は機械的判断。ProdLoggingOverrideModuleの 2 行で完結 - 半決定的: 「prod logger は何を出力すべきか」は本来非決定 (allowlist? redaction? per-endpoint? off?)。Slice 3 では 最も保守的 な「完全 off」を選択。Slice 4 以降で需要が出たら redacted logger を追加検討
- 未着手 (Slice 4 以降): env-driven entry point (
APP_CONTEXT=prodの bin 改修)、構造化 prod logger (audit trail 用途で何かしらは要る)、Fake → 実 DB の差し替え用ProdReasonModule
今後の Phase B Slice (推奨順)
| 候補 | 性質 | 一行コメント |
|---|---|---|
| 決定的 | ✅ 完了 (下記 Slice 4 セクション参照) | |
| 決定的 | ✅ 完了 (下記 Slice 5 セクション参照) | |
| Slice 6: AUTHZ check (Pilot 5 F-1) | 非決定的 | CheckoutPrepared に SessionGuard Reason 追加。session 設計と絡む |
| Slice 7: bear-security-setup | 半決定的 | 認証 / 認可基盤の skill 適用 |
| Slice 8: CSRF token | 非決定的 | 全 onPost endpoint に手を入れる |
| Slice 9: Taint annotation (Slice 1 の続き) | 非決定的 | Psalm を BEAR/Be 用に annotate |
Phase B — Slice 4: Mass-assignment fix (Pilot 5 F-2)
目的: Pilot 5 security-review F-2 (MASS-ASSIGNMENT) の構造的修正。
Problem (F-2 抜粋)
client supplied
paymentMethodIdをOrderEntity.paymentMethodIdと照合せず gateway に forward。決済方法のすり替えが可能。
doCheckout の前段 (doProceedToConfirm) で確定した決済方法を、confirm 段階で client が別の paymentMethodId (例: 安価 / 未認証の方式) に差し替えても通っていた。
Fix
「client から paymentMethodId を受け取らない」 に変更。サーバ側で永続化済みの OrderEntity.paymentMethodId を採用する。
| 変更ファイル | 変更内容 |
|---|---|
be/src/Input/CheckoutInput.php |
コンストラクタから public int $paymentMethodId を削除。docblock に「client から受け取らない理由 (mass-assignment 防止)」を明記 |
be/src/Being/CheckoutPrepared.php |
#[Input] public int $paymentMethodId 削除 |
be/src/Being/CheckoutSettled.php |
#[Input] public int $paymentMethodId 削除。$gateway->checkout($preOrderId, $paymentMethodId, ...) を $gateway->checkout($preOrderId, $order->paymentMethodId, ...) に変更 |
src/Resource/Page/Shopping/Checkout.php |
onPost(string $preOrderId, int $paymentMethodId) → onPost(string $preOrderId)。docblock に F-2 言及追加 |
tests/Resource/CheckoutResourceTest.php |
全 POST body から 'paymentMethodId' => N を削除。新規テスト: testClientSuppliedPaymentMethodIdIsIgnored — paymentMethodId=9 (本来 decline を起こす値) を client から injection しても 201 を返すこと (ResourceObject が paymentMethodId を Input にバインドしないため、key は黙って捨てられる) を assert |
be/tests/Domain/CheckoutCompletedTest.php |
全 new CheckoutInput(..., paymentMethodId: N) から paymentMethodId を削除 |
tests/Module/ProdModuleTest.php |
POST body から paymentMethodId を削除 |
be/src/Final/CheckoutCompleted.php は元から $order->paymentMethodId を使っていたため変更不要。
Test Suite の偶然の幸運
be/var/fake/orders.json の fixture が 元々 preOrderId → paymentMethodId を 1 対 1 に定めていた:
aaaa...→paymentMethodId: 2(success)bbbb...→paymentMethodId: 1(insufficient stock test 用)cccc...→paymentMethodId: 9(gateway decline test 用)
つまり以前から 「テストで指定していた paymentMethodId は OrderEntity と一致していた」。Slice 4 は invariant を「契約として明文化」しただけ。テストの assertion 値 (total 2250, captures[paymentMethodId]=2 等) は一切変更不要。
動作確認
composer test: 57/57 pass, 155 assertions, 0 notices (Phase A 既存 52 + Slice 3 新規 4 + Slice 4 新規 1)composer psalm: errors なし (MissingOverrideAttribute等の info 24 件のみ; baseline 内)composer psalm-taint: errors なし
Slice 4 の振り返り (決定的/非決定的)
- 決定的だった: 「client supplied PII を信頼しない」は OWASP / mass-assignment の textbook ルール。修正方針は一意 (= client field を削除して server-side source に置換)
- 設計上の含意: Be Framework の
#[Input]は 「client から受け取る値の宣言」 であり、これを削るだけで攻撃面が物理的に消える。BEAR ResourceObject のonPostシグネチャからも引数が消えるため、ResourceObject のバインダーが余計な key を黙って捨ててくれる (=Mass-assignment攻撃が 型シグネチャ違反として表現される ことが構造的に保証される) - 対称的な気づき: Slice 3 (logger 切替) が「書く側の構造的封じ込め」だったのに対し、Slice 4 は「読む側の構造的封じ込め」。両方ともコードを書き換えずに OFF にできる位置に security 境界がある (= AAA Module / Input 型)。これが Be Framework + BEAR.Sunday を「決定的要素として置ける」と言える根拠の 1 つ
残課題 (Slice 4 由来)
PaymentMethodFactoryInterface経由の表示用 ID:doProceedToConfirm段階で client が支払い方法を 選択する UI は別途必要。Pilot 5 fixture は scope 外なので未確認だが、その段階での paymentMethodId 取得経路 (session? form post? AJAX?) は Slice 6 (AUTHZ) と合わせて再設計する- double-submit / replay 攻撃: Slice 4 は payment method の固定化のみ。同じ preOrderId を 2 回送ると依然 2 回 gateway を呼べる (F-3 IDEMPOTENCY)。これは Slice 6+ で
dtb_order.pre_order_idUNIQUE +INSERT ON CONFLICTに再設計する
Phase B — Slice 5: env-gated entry point
目的: Slice 3 で作った ProdModule を 実際に起動経路から呼ばせる。それまでは ProdModuleTest が in-process で binding を検証しているだけで、CLI / HTTP の entry point は存在しなかった (Pilot 1-5 は全部 PHPUnit 経由)。
Why now (Slice 順序の判断)
Slice 3 で「ProdModule は AppModule の安全な置き換え」を 証明済み。Slice 4 で「Input 側の構造防御」も導入した。あとは どこからどうやって APP_CONTEXT=prod をフリップさせるか を実装するだけ。これが無いと「prod の binding はあるが、それを呼ぶ場所が無い」状態。Slice 6+ (AUTHZ / CSRF) も entry point 越しに振る舞いを観測したくなる場面が出るので、ここで足場を作る。
追加ファイル (3)
| ファイル | 役割 |
|---|---|
| bin/app.php | CLI entry。APP_CONTEXT → Module class ({name}Module, ucfirst) を resolve し、Injector で ResourceInterface を解決。page://self/... URI と JSON body 引数を受け取って resource を実行、結果を JSON で stdout に出力。失敗時は exit 2 |
| public/index.php | HTTP entry。同じ context resolution を行い、REQUEST_METHOD + REQUEST_URI + JSON body から resource 呼び出しに変換、JSON で response 返す。Slice 5 では minimum viable (router / AOP / cache 全部なし)。本番運用には不足だが「prod context が起動経路で発動する」ことの実証としては十分 |
| tests/EntryPoint/AppEntryPointTest.php | bin/app.php を subprocess (exec()) で起動して APP_CONTEXT が 本当に効いている ことを検証。prod で log 未書き出し、app で書き出し、未定義 context で exit 2、URI 欠落で exit 2 の 4 ケース |
Context resolution 規約
APP_CONTEXT=prod → MyVendor\BeMart\Module\ProdModule
APP_CONTEXT=app → MyVendor\BeMart\Module\AppModule
APP_CONTEXT 未設定 → AppModule (dev default)
実装は ucfirst($context) . 'Module' で class 名を生成 → class_exists + is_subclass_of(AbstractModule) で安全性チェック → new $class($meta) で instantiate。BEAR\Package\Injector の Module クラス (vendor/bear/package/src/Module.php) と同じ convention を採用したが、Injector::getInstance 自体は使わなかった (理由: AppInterface binding を要求するが、Pilot 5 までの module は AppInterface を bind していない。Slice 5 の scope を逸脱するので深追いせず、Ray\Di\Injector を直接使う簡易版で済ませた)。
検証
$ rm -f var/log/bemart.json
$ APP_CONTEXT=app php bin/app.php 'page://self/shopping/checkout' '{"preOrderId":"aaaa00000000000000000000000000000000aaaa"}'
{ "context": "app", "uri": "...", "code": 201, "body": { ... } }
$ ls var/log/bemart.json
-rw-r--r-- ... bemart.json # ← 書かれた
$ rm -f var/log/bemart.json
$ APP_CONTEXT=prod php bin/app.php 'page://self/shopping/checkout' '...'
{ "context": "prod", ... }
$ ls var/log/bemart.json
ls: ... No such file or directory # ← 書かれていない
composer test: 61/61 pass, 166 assertions, 0 notices (Slice 4 まで 57 + entry-point 新規 4)composer psalm: errors なしcomposer psalm-taint: errors なし
設計上の判断と注釈
- HTTP server を一切設定していない:
public/index.phpは WebRouter / Compiler / cache 一切なし。本番デプロイ前に Slice 7 (bear-security-setup) と合わせて router + 認証 middleware を埋める前提 - Psalm scope 外:
bin//public/はpsalm.xmlのprojectFilesに含めていない。理由は (a) 元々 procedural script は$_SERVER/$_POSTでmixedが多発し noise になりがち、(b) entry point の logic 自体は 50 行程度で目視確認可能。Slice 9 (taint annotation) で$_POSTを taint source として正しく扱うときに合わせて scope に入れる - テストは subprocess fork:
exec()で別 PHP プロセスを起動。理由は (a)getenvの親プロセスとの汚染を避ける、(b) DI cache が共有されない、(c) 「本当に起動経路から呼べる」ことの証明。代償として 4 件のテストで約 1 秒余計にかかる bin/smoke_phase6.php: 残置。これは Pilot 2 (AddCartItemInput) 用の手元 smoke runner で、bin/app.phpの汎用版とは別物。削除はしない (Pilot 2 を再現する手っ取り早い手段として有用)
Slice 5 の振り返り (決定的/非決定的)
- 決定的だった: 「env 変数で Module を switch する」 のメカニズム自体は 完全に固定パターン。BEAR\Package\Module の class-name convention をコピーするだけ。Slice 1 (Psalm setup) より更に意思決定が少ない
- 判断が要ったポイント (= 半決定的):
- どこまで作るか: 完全 router 付きの HTTP front controller か、それとも env switch + 最低限の dispatch か。Slice 5 の目的「prod がアクセス経路から発動する」 を満たすだけなら後者で十分 → 後者を選択
- Injector の API 選択:
BEAR\Package\Injector::getInstance(Compiler 付き) かRay\Di\Injector(直接) か。前者はAppInterfacebinding を要求するため、Slice 5 でやるには Pilot 全部に AppInterface を入れる必要があった → 後者で済ませた
- 積み残しの自覚: Slice 5 を経ても 「production-grade な entry point」 ではない。Router (URI → Resource binding) / Compiler (eager AOP weaving) / Logger sink / Error formatter のどれも欠けている。Slice 7 (bear-security-setup) / 将来の Slice (Phase C 相当の deployment 系) で順次埋める
次の Slice (Slice 6 以降)
| 候補 | 性質 | 一行コメント |
|---|---|---|
| Slice 6: AUTHZ check (Pilot 5 F-1) | 非決定的 | CheckoutPrepared に SessionGuard Reason を追加。session の保持方法 (cookie / JWT / server-side store) は未決定なのでユーザー判断が必要 |
| Slice 7: bear-security-setup | 半決定的 | 認証 / 認可基盤の skill 適用。Slice 6 と一部統合検討 |
| Slice 8: CSRF token | 非決定的 | 全 onPost endpoint に token middleware。token 配布手段 (session vs sync) は要判断 |
| Slice 9: Taint annotation | 非決定的 | Psalm @psalm-taint-* の BEAR #[Input] 対応。$_POST を taint source として正しく扱う |
Phase B — Slice 6: AUTHZ check (Pilot 5 F-1)
目的: Pilot 5 security-review が指摘した F-1 (AUTHZ 欠如) を閉じる。Resource は preOrderId を受け取るだけで「その pre-order が requester のものか」を一切確認していなかった。?preOrderId=... を URL から拾えば誰でも他人の確定を実行できた状態。
Why now (Slice 順序の判断)
Slice 5 で entry point が動いたので、Slice 6 は「session という非決定的な要素を どこに 持つか」だけ決めれば実装に入れる。CSRF (Slice 8) / Taint (Slice 9) より前にやる理由は: AUTHZ は business invariant (誰がオーナーか) で、CSRF は transport invariant (この request が本物か)。前者が無いと後者があっても他人の pre-order は確定できてしまう。順序として AUTHZ → CSRF が正しい。
追加 / 変更ファイル
| ファイル | 役割 | 種別 |
|---|---|---|
be/src/Reason/Service/SessionInterface.php(旧パス) |
customerId(): string\|null のみを持つ最小 contract。Be Reason として注入される |
新規 |
be/src/Reason/Service/FakeSession.php(旧パス) |
constructor で渡された customerId をそのまま返す。null を渡せば anonymous |
新規 |
| be/src/Exception/UnauthorizedPreOrderAccessException.php | DomainException 派生 + #[Message] で en/ja を持つ。Resource は HTTP 403 にマップ |
新規 |
| be/src/Being/CheckoutPrepared.php | SessionInterface を #[Inject] し、$session->customerId() !== $order->customerId で reject。順序: 存在 → AUTHZ → PurchaseFlow。理由は本文参照 |
変更 |
| src/Resource/Page/Shopping/Checkout.php | UnauthorizedPreOrderAccessException を Code::FORBIDDEN (403) にマップ。docblock 更新 |
変更 |
| src/Module/AppModule.php | bind(SessionInterface)->toInstance(new FakeSession('customer-001'))。default を logged-in customer-001 に固定 することで既存 Pilot テストを破壊しない |
変更 |
| be/tests/Domain/CheckoutCompletedTest.php | rebindSession() helper を追加。cccc… (customer-002) を扱う既存テスト + 2 件の AUTHZ 失敗テストを追加 |
変更 |
| tests/Resource/CheckoutResourceTest.php | 同じパターン。403 を返す 2 件追加 | 変更 |
設計上の判断
Session の保持方法 — 「Reason として注入」 を選択
候補は 3 つあった:
$_SESSIONglobal を直接読む procedural helper — Be / BEAR どちらの DI 哲学にも合わない。テストで session を切替えるのも難しい。却下SessionInterfaceを#[Inject]する Reason — Be Reason として扱う。Fake は memory、本番は$_SESSION読みアダプタを後付け差し替え。採用- Resource 層で読んで Input に乗せる —
CheckoutInputにcustomerIdを足す。クライアントが指定できる field を増やすことになり Slice 4 で潰した mass-assignment と矛盾。却下
順序: 存在 → AUTHZ → PurchaseFlow
1. OrderQuery::byPreOrderId(...)
→ null なら PreOrderNotFoundException (404)
2. $session->customerId() !== $order->customerId
→ throw UnauthorizedPreOrderAccessException (403)
3. PurchaseFlow::apply($order)
→ totals 計算 (失敗時 InsufficientStock 422)
3 つの理由:
- (存在) が先: もし AUTHZ を先にしてしまうと、anonymous user が
?preOrderId=<random>で総当たりした場合に「403 = 存在する」「404 = 存在しない」の 存在オラクル ができてしまう。存在チェックを先にすれば、anonymous は常に「404 か 403 か」のどちらか (= 同じ 4xx の塊) しか観測できない。ただし完璧ではない: customer-001 がログイン中に他人の preOrderId を試した場合は「404 (存在しない)」 vs 「403 (存在するが他人のもの)」 で区別できる。これは F-1 の scope 外 (timing attack 系) として残置。Phase B でログイン中の存在チェックも 404 に統一する判断は要相談 - (AUTHZ) が PurchaseFlow より先: PurchaseFlow は (将来) 在庫引当の前計算 / 配送料計算を含む。AUTHZ を後にすると認可されない request でも compute が走る。DoS amplification を避ける
- PurchaseFlow が最後: ここまで来れば requester は本物のオーナー。失敗は純粋に business reason (在庫無し等)
Default を customer-001 に固定した理由
AppModule の default SessionInterface を new FakeSession('customer-001') に固定。これにより 既存 Pilot 1-5 のテスト (aaaa… を使う happy-path 系) は 1 行も変更不要。AUTHZ をテストしたいテストだけが override する。
代替案として「default を anonymous (null) にする」 もあった。これは「security by default」 として正しい設計だが、Pilot 1-5 の 50+ テストすべてに rebindSession を入れる必要があった。Slice 6 の scope を肥大化させるので却下。本番では Slice 7 (bear-security-setup) で $_SESSION アダプタに差し替えるので、AppModule の default 値はあくまで test fixture。
Code::FORBIDDEN (403) と Code::NOT_FOUND (404) の差
OWASP の AUTHZ guide に従い、「リソースが存在することは知っているが、あなたには見せない」 を 403、「リソースは存在しない (またはあなたに見せない、を兼ねる)」 を 404 とした。Pilot 5 では:
- 404:
OrderQueryが null → 物理的に存在しない - 403: 存在するが
customerIdが一致しない → AUTHZ 違反
これは設計上の選択であり、上記の timing attack 注釈の通り完璧な存在隠蔽ではない。Slice 6 では「明確な区別」 を取り、Slice 7+ で「存在隠蔽の強化」 を別途検討する。
Test 戦略: rebindSession() helper
PHPUnit の setUp() で default customer-001 を bind、各テストで必要なら rebindSession(...) で injector を作り直す。理由:
- Pilot 5 既存テスト (
aaaa…/bbbb…= customer-001) は default で動くまま → 既存テスト 0 行変更 - payment-decline (
cccc…= customer-002) は 1 行rebindSession('customer-002')を足すだけ - AUTHZ 失敗テスト は
rebindSession('customer-999')/rebindSession(null)を足す
新規 AUTHZ テスト 4 件:
| テスト名 | session | preOrderId | 期待結果 |
|---|---|---|---|
testForeignCustomerRejectedWithAuthz |
customer-999 |
aaaa… (owner: customer-001) |
UnauthorizedPreOrderAccessException + no side-effect (gateway captures / mailer sent が 0 件のまま) |
testAnonymousSessionRejectedWithAuthz |
null |
aaaa… |
同上 |
testOnPostForeignCustomerReturns403 |
customer-999 |
aaaa… |
HTTP 403 |
testOnPostAnonymousReturns403 |
null |
aaaa… |
HTTP 403 |
no side-effect の検証 が重要。AUTHZ は CheckoutPrepared (Stage 1) で reject されるので、CheckoutSettled (Stage 2: payment capture + order number 採番) と CheckoutCompleted (Final: persist + mail + cart-clear) には到達しない。$gateway->captures() と $mailer->sent() を直接 introspect して確認。
検証
$ composer test
OK (65 tests, 172 assertions) # Slice 5 から +4 (新規 AUTHZ 4 件)
$ composer psalm
No errors found!
$ composer psalm-taint
No errors found!
cccc… を使う既存テスト 2 件 (testPaymentDeclinedRejected / testOnPostPaymentDeclinedReturns422) は AUTHZ が先に走るようになったため一度 fail したが、rebindSession('customer-002') の追加で復旧。これは 新規 AUTHZ が想定通り効いている 証跡でもある (orders.json で cccc… が customer-002 にひもづいているのは Pilot 3 時の元データ)。
Slice 6 の振り返り (決定的/非決定的)
- 非決定的だった部分: 「session の保持方法」 と 「default value」。前者は Reason として注入 で決着、後者は test 互換性を優先して customer-001 固定。どちらも Slice 7 (bear-security-setup) で 本番アダプタ + 真の default は anonymous に置き換える前提
- 決定的だった部分: 順序 (存在 → AUTHZ → PurchaseFlow) は OWASP / DoS 観点から論理的に 強制された。Resource → 403 のマッピングも HTTP semantics から自明
- 積み残し:
- 本番 SessionInterface アダプタ (
$_SESSION読み or JWT decode or cookie-based) は Slice 7 で実装。SessionInterfaceという contract だけ確定させて、実装は差し替え可能にしてある - 存在オラクル: ログイン中 user が他人の preOrderId を試したときの 404 vs 403 区別は残置。Slice 8+ で「ログイン中も 404 に統一」 を検討
- logged-out user の HTTP 401 vs 403: anonymous (
customerId()===null) は技術的には「authentication が無い」ので 401 が正解。Slice 6 では「ownership 不一致」 として 403 で統一した。Slice 7 で AuthN/AuthZ を分離する時に 401 / 403 を分ける可能性あり
- 本番 SessionInterface アダプタ (
次の Slice (Slice 7 以降)
| 候補 | 性質 | 一行コメント |
|---|---|---|
| Slice 7: bear-security-setup + 本番 Session アダプタ | 半決定的 | bear-security-setup skill 適用 + $_SESSION (または JWT) → SessionInterface の本番実装。ProdModule 側で差し替え |
| Slice 8: CSRF token | 非決定的 | 全 onPost endpoint に CSRF guard。token 配布手段 (session-bound vs sync token) は要判断 |
| Slice 9: Taint annotation | 非決定的 | Psalm @psalm-taint-* を Pilot 1-5 + Slice 6 にも適用。$_POST / $_SESSION を source、gateway::charge 等を sink としてグラフを引く |
| (追加検討) Slice 10: 存在オラクル軽減 | 非決定的 | 既存 user に対する 404 / 403 統一。user-facing UX を悪化させる ので、threat model と合わせてユーザー判断が必要 |
Phase B — Slice 7: 本番 Session アダプタ (EC-CUBE bridge, BEAR 側のみ)
目的: Slice 6 で導入した SessionInterface の 本番実装の BEAR 側半分 を ProdModule 配下に与える。Slice 6 までは FakeSession('customer-001') が全 context で動いており、本番でも全員 customer-001 扱い = AUTHZ 素通り状態だった。Slice 7 で EccubeSharedSessionAdapter を ProdModule の override として bind し、本番経路では 実 session が空なら anonymous になる。
⚠️ 実運用にはまだ届いていない: ブリッジ契約のうち BEAR 側 (
$_SESSIONを読む) のみが実装済み。EC-CUBE 側 (login 時に authenticated customerId を flat key へミラーする EventListener) は 未実装。それが入るまで、本番 HTTP リクエストは全て anonymous → AUTHZ が全部 403 を返す状態。Slice 7 は「BEAR 側の半分」と「契約の確定」 までを担う。残りは Phase 2 (EC-CUBE 移植) で着地する。
Why now (Slice 順序の判断)
Slice 6 (AUTHZ) は test 上だけで完全に機能していた。本番では ProdModule が AppModule を install し、AppModule 内の bind(SessionInterface)->toInstance(new FakeSession('customer-001')) が活きてしまうため AUTHZ は無効化される。Slice 7 が無いと Slice 6 は飾り。それより前に CSRF (Slice 8) を入れても、AUTHZ が無効なら他人の pre-order を確定できる事実は変わらない。順序は AUTHZ binding 本物化 → CSRF → 残り、で確定。
ただし Slice 7 の BEAR 側だけでは Slice 6 の AUTHZ は まだ実用にならない。Slice 7 が片付けたのは「FakeSession('customer-001') が本番に漏れて全員素通りになる」というクリティカルな failure mode。残課題は「実 customerId を実際に検証する経路」 で、これは EC-CUBE 側ハーネス (後述) が入った時点で完成する。それまでは「fail closed (全部 403)」 = 安全側に倒した状態。
ユーザー判断 (Slice 7 着手前に必要だった)
「Session の保持方法」 3 択を提示:
| option | dep | EC-CUBE 側変更 |
|---|---|---|
| A. Cookie (BEAR 独自) | symfony/http-foundation 必要 | 不要 (BEAR 側で完結) |
| B. JWT | firebase/php-jwt 等 | 不要 (BEAR 側で完結) |
| C. 既存 EC-CUBE の Symfony Session 継承 ← 採用 | BEAR 側 dep ゼロ | 5 行の EventListener 追加 |
C を選択した理由 (ユーザー判断): 移行期は EC-CUBE と BEAR が並存する。「BEAR 側に独自 session を建てる」 = 二重ログイン or 二重 session sync が必要 = 移行コスト大。EC-CUBE が既に管理している session を共有し、BEAR は読むだけにする方が現実的。
C の内部にもう 1 サブ判断: 「customerId をどこから読むか」:
- A1. Flat key
$_SESSION['customer_id']を読む ← 採用 - A2. Symfony Security の serialized Token (
$_SESSION['_security_main']) を unserialize → symfony/security-core 依存追加 - A3. Sidecar HTTP
/api/me→ 1 RTT 追加
A1 (Recommended) を選択: BEAR 側に Symfony Security 依存をゼロにする ことを優先。EC-CUBE 側に 5 行の EventListener (login 時に flat key へ mirror、logout 時に unset) を入れれば完結。Symfony Security の internal class signature 変更にも引きずられない (これは Symfony 5 → 6 → 7 で実際に起きてる) 。
追加 / 変更ファイル
| ファイル | 役割 | 種別 |
|---|---|---|
| src/Auth/EccubeSharedSessionAdapter.php | SessionInterface の本番実装。$_SESSION → flat key 読み + CLI fallback (env var) + headers-sent 防御 |
新規 |
| src/Module/ProdSessionOverrideModule.php | bind(SessionInterface)->to(EccubeSharedSessionAdapter)。ProdModule から override として install |
新規 |
| src/Module/ProdModule.php | $this->override(new ProdSessionOverrideModule()) を 1 行追加 |
変更 |
| tests/Auth/EccubeSharedSessionAdapterTest.php | adapter 単体: 7 ケース (session 読み / anonymous / CLI env fallback / session 優先 / 空文字 / 非 string / custom key) | 新規 |
| tests/Module/ProdModuleTest.php | 既存 happy-path テストに $_SESSION['customer_id']='customer-001' 注入 + tearDown でクリア |
変更 |
| tests/EntryPoint/AppEntryPointTest.php | prod CLI に BEMART_CLI_CUSTOMER_ID env 注入 + anonymous CLI が 403 になることの negative test 追加 |
変更 |
EC-CUBE 側 contract (Slice 7 では実装しない)
BEAR 側は 読むだけ。EC-CUBE 側に以下のミラーを入れることでブリッジが完成する:
// EC-CUBE 側 EventListener (例: app/Customize/EventListener/SessionMirrorListener.php)
public function onLoginSuccess(InteractiveLoginEvent $event): void
{
$customer = $event->getAuthenticationToken()->getUser();
if ($customer instanceof \Eccube\Entity\Customer) {
// BEAR 側 EccubeSharedSessionAdapter::CUSTOMER_ID_KEY と一致させる
$event->getRequest()->getSession()->set('customer_id', (string) $customer->getId());
}
}
public function onLogout(LogoutEvent $event): void
{
$event->getRequest()->getSession()->remove('customer_id');
}
- Cookie 名: EC-CUBE 4.x のデフォルト
ECCUBE(=EccubeSharedSessionAdapter::COOKIE_NAME) - Flat key:
customer_id(=EccubeSharedSessionAdapter::CUSTOMER_ID_KEY) - BEAR が同じドメイン / path で動作することが前提 (= cookie 共有可能なデプロイ構成)
この EC-CUBE 側ハーネスは EC-CUBE 移植 (Phase 2 以降) で実装される。Slice 7 は contract だけ確定させる。
CLI fallback (運用スクリプト用)
bin/app.php から ProdModule を呼ぶ場合、SAPI=cli で HTTP session が無い。BEMART_CLI_CUSTOMER_ID env var を設定すれば、adapter はそれを authenticated customerId として返す。env が無ければ anonymous = AUTHZ で 403。subprocess test の testProdContextRejectsAnonymousCli がこの contract を pin している。
⚠️ この env var は authentication をバイパスする。HTTP context で絶対に設定してはいけない (adapter 側で PHP_SAPI === 'cli' をガードしているが、php-fpm 内で誤って setenv される可能性を残さないこと)。
検証
$ composer test
OK (73 tests, 181 assertions) # Slice 6 から +8 (adapter unit 7 + entry-point anon 1)
$ composer psalm
No errors found!
$ composer psalm-taint
No errors found!
設計上の判断
「Symfony Session 継承」 の正確な意味
このリポジトリには EC-CUBE 本体も symfony/http-foundation も入っていない (= 入っているのは symfony/cache, symfony/console の transitive dep のみ)。 そのため Slice 7 で言う 「継承」 は 「実 EC-CUBE のセッション形式を物理的に共有する契約」 であって 「Symfony コンポーネントを依存に入れる」 ことではない。Adapter は PHP 標準 $_SESSION だけを使う。
これにより:
- BEAR 側 dep ゼロ (Slice 7 は composer.json を 1 行も触らない)
- EC-CUBE の Symfony version を BEAR が知らなくていい
- 単体テストが Symfony 依存を持たない (
$_SESSIONを poke するだけ)
Override を 2 段重ねる構造
ProdModule (configure)
├── install(AppModule) ← 既存 dev binding 一式
├── override(ProdLoggingOverrideModule) ← Slice 3: PII log fix
└── override(ProdSessionOverrideModule) ← Slice 7: Session binding 切替
ProdLoggingOverrideModule (Slice 3) のパターンをそのまま踏襲。1 つの override module = 1 つの責務 にすることで、Slice 4-9 で増えていく差し替え bindings (CSRF / rate-limit / 本番 DB / 本番 Mailer / …) を独立にレビューできる。ProdModule.configure() は将来 5-6 行の override 呼び出しが並ぶことを許容する設計。
Adapter の resolution 優先順位 (session 優先 → CLI env fallback)
1. $_SESSION['customer_id'] が string で空でない → return
2. PHP_SAPI === 'cli' && BEMART_CLI_CUSTOMER_ID env が空でない → return
3. else null (anonymous)
$_SESSION を CLI でも優先 する。理由は PHPUnit 内 ProdModuleTest が $_SESSION を poke する pattern を取るため。CLI 環境でも session 値を honor することで、test harness と運用スクリプトで同じ adapter を使い続けられる。本番 HTTP context では env var は無視されるので「session が無いのに env で詐称できる」 攻撃ベクタは生まれない (= prerequisite: PHP_SAPI === 'cli' で $_SESSION が空、という二重条件)。
bin/app.php の exit code
bin/app.php は 4xx → exit 1 で動く (Slice 5 時に決定)。Slice 7 で追加した testProdContextRejectsAnonymousCli も exit=1 を期待する。これで「subprocess が走った (exit 2 = script error ではない)」 かつ 「resource が 403 で reject した」 を別々に assert できる。
Slice 7 の振り返り (決定的/非決定的)
- 決定的だった:
- 「Slice 6 binding の本番化が必要」 という議論の余地ゼロ
ProdLoggingOverrideModuleのパターンを踏襲する (1 module = 1 責務)- PHP
$_SESSIONを使う (Symfony dep を入れない)
- 非決定的だった (= ユーザー判断要):
- 「Symfony Session 継承」 の正確な意味 (A. cookie / B. JWT / C. EC-CUBE 共有) — C 採用
- 「customerId をどこから読むか」 (Flat key / Symfony Token / Sidecar API) — Flat key 採用
- 積み残し:
- EC-CUBE 側 EventListener の実装 — これが入るまでは本番経路 = 全 anonymous = AUTHZ 全 403。Slice 7 単体では「production-ready な auth path」 ではない。Phase 2 (EC-CUBE 移植 slice) で着地させる
- 本番 HTTP server — Slice 5 の
public/index.phpはまだ router 無しの最小実装。real-world deploy 前に WebRouter + Compiler + error formatter が必要 - Logout 動線 — adapter は read-only。BEAR 側に明示 logout endpoint は無い。Phase 2 で EC-CUBE の /mypage/logout を経由するルートを確認
- Session security 強化 — adapter は
use_strict_mode/cookie_httponly/cookie_samesite=Laxを session_start に渡しているが、cookie_secure(HTTPS only) は php.ini 任せ。本番デプロイ時にcookie_secure=1を必ず設定すること BEMART_CLI_CUSTOMER_IDenv var の正当性 — Slice 7 内の subprocess test (testProdContextRejectsAnonymousCliの対) を成立させるために導入された CLI 専用バイパス。PHP_SAPI === 'cli'でガードしているが、運用スクリプトを増やさないなら将来削除候補
次の Slice (Slice 8 以降)
Slice 8 は本 HANDOVER 内の “Phase B — Slice 8: CSRF token” セクションを参照。
| 候補 | 性質 | 一行コメント |
|---|---|---|
| Slice 9: Taint annotation | 半決定的 | Psalm @psalm-taint-* を Pilot 1-5 全体に適用。$_POST / $_SESSION source → gateway::charge 等 sink の graph。CSRF check site は taint sanitizer として annotate する |
| (追加検討) Slice 10: 存在オラクル軽減 | 非決定的 | 既存 user に対する 404 / 403 統一。今や CSRF と AUTHZ の両方が 403 を返すので、404 → 403 の方向で揃えるか判断が必要 |
| (追加検討) bear-security-setup skill 適用 | skill bake | Slice 6-7-8 で手動構築した AUTHZ + CSRF 基盤を skill 化するレビュー。bear-skills 側に knowledge を回す |
Phase B — Slice 8: CSRF token (BEAR 側のみ)
全 state-changing onPost (Cart/Item, Entry, Shopping/Checkout) で csrfToken を検証する Resource-boundary guard を導入。Slice 6-7 の AUTHZ 基盤に直交する layer として追加。
Why now (Slice 順序の判断)
Slice 7 で確立した「BEAR 側のみ実装 + EC-CUBE 側 EventListener は Phase 2」のパターンに最も近い slice。両者ともに session-shared な flat key を読むだけの adapter で構造が一致するため、Slice 7 の review knowledge を即座に再利用できる。Slice 9 (Taint annotation) より先に着手したのは、Slice 9 の sink/source graph に CSRF check site も含めるべきだから (実装後にまとめて annotate する方が手戻りが少ない)。
追加 / 変更ファイル
新規:
be/src/Reason/Service/CsrfToken.php—isValid(string|null): boolだけを公開する domain-facing interface。token 発行は対象外。be/src/Reason/Service/FakeCsrfToken.php— dev/test、固定 token (FakeCsrfToken::TOKEN) とhash_equals比較。src/Auth/EccubeSharedCsrfTokenAdapter.php— prod、$_SESSION['_csrf_token']を読む。CLI fallback はBEMART_CLI_CSRF_TOKENenv var (Slice 7 と同じ pattern)。src/Module/ProdCsrfOverrideModule.php— prod 専用CsrfToken → EccubeSharedCsrfTokenAdapterbinding。tests/Auth/EccubeSharedCsrfTokenAdapterTest.php— 11 ケースの unit test。
変更:
src/Module/AppModule.php—CsrfToken → FakeCsrfToken(Singleton)。src/Module/ProdModule.php—ProdCsrfOverrideModuleinstall + Slice 7.1 で古い名前のまま残っていた “SymfonySessionAdapter” doc 参照をEccubeSharedSessionAdapterに修正。src/Resource/Page/Cart/Item.php—csrfToken引数を受け取り、onPost先頭で$csrf->isValid()検証、失敗時 403。src/Resource/Page/Entry.php— 同上。src/Resource/Page/Shopping/Checkout.php— 同上。tests/Resource/{CartItem,Entry,Checkout}ResourceTest.php— 既存テストにcsrfToken追加 + 「missing CSRF → 403」 ケースを各 Resource に追加。tests/Module/ProdModuleTest.php—$_SESSION['_csrf_token']ミラー追加 +testProdContextRejectsMissingCsrfToken新規 + Slice 7.1 doc 残骸 (“SymfonySessionAdapter”) をEccubeSharedSessionAdapterに修正。tests/EntryPoint/AppEntryPointTest.php— subprocess test にBEMART_CLI_CSRF_TOKEN環境変数追加 +testProdContextRejectsMissingCsrfTokenCli新規。
EC-CUBE 側 contract (Slice 8 では実装しない)
EccubeSharedCsrfTokenAdapter の本番動作には EC-CUBE 側で以下の協力が必要:
-
Token mirror — Symfony Forms (
csrf_protection: true) が生成・保管している token を、状態変更フォーム render 時に$_SESSION['_csrf_token'](flat string) にコピーする EventListener / Twig extension。例:// app/Customize/EventListener/CsrfMirrorListener.php (EC-CUBE 側) public function onKernelResponse(ResponseEvent $event): void { if (! $event->isMainRequest()) { return; } $session = $event->getRequest()->getSession(); if (! $session->isStarted()) { return; } // intention id を 1 つに固定して mirror する (例: 'bemart_form') $token = $this->csrfManager->getToken('bemart_form')->getValue(); $session->set('_csrf_token', $token); } -
Token rotation — login / logout で必ず regenerate (
session_regenerate_id+ 新規 token mirror) する。Slice 7.2 (SessionMirrorListener) と統合する形で 1 つのリスナーにまとめる方が漏れにくい。
Slice 7.2 同様、これらは Phase 2 (EC-CUBE 移植) で着地させる。Slice 8 単体では「production-ready な CSRF path」ではない ($_SESSION['_csrf_token'] が空 → 全 POST 403 → fail-closed)。
CLI fallback (運用スクリプト用)
Slice 7 の BEMART_CLI_CUSTOMER_ID と全く同じ pattern。PHP_SAPI === 'cli' 限定で BEMART_CLI_CSRF_TOKEN env var を reference token として受け入れる:
APP_CONTEXT=prod \
BEMART_CLI_CUSTOMER_ID=customer-001 \
BEMART_CLI_CSRF_TOKEN=$(openssl rand -hex 16) \
php bin/app.php page://self/shopping/checkout \
"{\"preOrderId\":\"aaaa…\",\"csrfToken\":\"$BEMART_CLI_CSRF_TOKEN\"}"
HTTP context (PHP_SAPI !== 'cli') ではこの fallback は到達しないため、漏れたら全 POST が 403 になるだけで credential bypass にはならない。
検証
composer test— 90 tests / 205 assertions green (Slice 7.1 時点から +17 tests / +24 assertions)。新規ProdModuleTest::testProdContextRejectsMissingCsrfTokenとAppEntryPointTest::testProdContextRejectsMissingCsrfTokenCliが HTTP / CLI 両方の rejection path を pin している。composer psalm— errors なし。composer psalm-taint— errors なし (CSRF check そのものは taint sink ではないので新規 annotation 不要)。
設計上の判断
Token 保管: session-bound を採用
| 候補 | 採用 | 理由 |
|---|---|---|
A. session-bound ($_SESSION['_csrf_token']) |
採用 | Slice 7 の session 基盤を再利用、EC-CUBE/Symfony Forms と同じモデル、HMAC secret 管理不要、per-session rotation は session_regenerate_id で自然に達成 |
| B. stateless synchronizer (HMAC-signed cookie) | 不採用 | secret 管理 + rotation policy + Slice 7 の session-shared 方針との二重化 |
| C. double-submit cookie | 不採用 | Slice 7 で既に session を共有しているのに重ねる意味が薄い |
Validation site: Resource 層
CSRF は HTTP boundary concern であり、Be domain は HTTP origin を知らない。Slice 6 の AUTHZ ($session->customerId() !== $order->customerId) を CheckoutPrepared (Being) に置いたのは “ownership は domain rule” だからだが、CSRF は “request の正当性” であって domain rule ではない。
- AUTHZ: 「この customer がこの order を所有しているか」 ← business rule
- CSRF: 「この POST が我々のフォーム由来か」 ← transport rule
そのため CSRF check は Resource の onPost 先頭、Becoming 呼び出しより前に置いた。Be domain は CsrfToken を see できるが consult しない (Reason interface としては定義したが、actual call site は BEAR Resource 側のみ)。
Failure response: 403 (Code::FORBIDDEN)
候補: 400 / 403 / 422。RESTful 慣習では「認証は通ったが、この request は拒否」 が 403 に最も合致。OWASP CSRF Prevention Cheat Sheet も “Forbidden” を推奨。AUTHZ rejection も 403 を返すので、body の message で “CSRF” / “authorized” を区別する。
パラメータ名: csrfToken (PHP camelCase)
候補: _token (Symfony Forms 慣習) / csrf_token (snake_case) / csrfToken (camelCase)。BEAR は PHP の引数名 = body field 名なので、$_token を使うと PHP superglobal ($_GET 等) と視覚的に紛らわしい。EC-CUBE 側で legacy form (_token) と統合する必要が出たら、その時点で Resource 引数を rename するか、入力の renaming を 1 箇所で吸収する adapter を追加すれば良い。
Token issuance を interface に含めない
CsrfToken::isValid() のみ。current() / get() は持たない。理由:
- Slice 8 のスコープは「validation only」 — token 発行は form-render 時の責務であり、resource boundary では発生しない
- EC-CUBE 側で既に Symfony Forms が token を発行しているので、BEAR 側は読むだけ
- 将来 BEAR 単独 form を作るとしても、
CsrfTokenIssuerInterfaceを別 interface に切る方が ISP に従う
Slice 8 の振り返り (決定的/非決定的)
- 決定的だった:
- Slice 7 の adapter pattern を CSRF に複製する構造そのもの
hash_equalsfor timing-safe comparison- 失敗時の 403
- CLI fallback の env var pattern (Slice 7 と対称)
- 非決定的だった (= ユーザー判断要):
- Token 保管: session-bound vs stateless — A 採用
- Validation site: Resource vs Being — Resource 採用 (CSRF を domain rule から分離)
- パラメータ名:
_tokenvscsrfToken—csrfToken採用 (PHP camelCase 一貫性)
- 積み残し:
- EC-CUBE 側 Token mirror EventListener — Slice 7.2 と同様、本番経路では reference token が空 → 全 POST 403。Phase 2 で Slice 7.2 と統合実装。
- Token rotation policy の明文化 — login / logout / form-submit-once の各タイミングで token を rotate するか、session 単位で固定するか。Phase 2 で EC-CUBE 側実装と合わせて決定。
BEMART_CLI_CSRF_TOKENenv var の正当性 — Slice 7 のBEMART_CLI_CUSTOMER_IDと同じ判断点。運用スクリプトを増やさないなら将来削除候補。Code::FORBIDDENを AUTHZ と CSRF で共有 — 両方 403 を返すため、本来は client side で区別ができない。OWASP も 403 を推奨しているのでこれで良いが、observability 上はmessagefield の文字列でしか区別できない点を運用側で認識する必要あり。
次の Slice (Slice 9 以降)
Slice 9 は本 HANDOVER 内の “Phase B — Slice 9: Taint annotation” セクションを参照。
| 候補 | 性質 | 一行コメント |
|---|---|---|
| (追加検討) Slice 10: 存在オラクル軽減 | 非決定的 | 既存 user に対する 404 / 403 統一。今や CSRF と AUTHZ の両方が 403 を返すので、404 → 403 の方向で揃えるか判断が必要 |
| (追加検討) Slice 11: Be Framework 用 Psalm plugin | 非決定的 | Slice 9 で発覚した「#[Be] chain が Psalm に opaque」 問題への対策。plugin で #[Be] を辿るか、per-class manual propagation を組むか |
| (追加検討) bear-security-setup skill 適用 | skill bake | Slice 6-7-8 で手動構築した AUTHZ + CSRF 基盤を skill 化するレビュー |
| Slice 7.2 / 8.2 統合: EC-CUBE 側 EventListener | Phase 2 入口 | customer_id mirror (Slice 7.2) + _csrf_token mirror (Slice 8.2) を 1 つの Symfony EventListener にまとめる。Phase 2 のキックオフ slice として扱う |
Phase B — Slice 9: Taint annotation (Pilot 1-5 全体)
Resource boundary に @psalm-taint-source、Reason interface の sink 候補に @psalm-taint-sink を導入し、composer psalm-taint を「真の audit graph」へ近づける slice。Slice 1 で scaffolding した taint analysis に「BEAR/Be flow に合わせた contract」を後付けする位置づけ。
Why now (Slice 順序の判断)
Slice 8 で確立した「Resource onPost 引数 = user input boundary」 のおかげで、source の位置が明確になっている (csrfToken 含む全 onPost 引数)。先に Slice 10 (存在オラクル) に進む手もあったが、Slice 10 は user observability の判断であって Phase B のセキュリティ強化路線とは別軸。Slice 9 を先に閉じる方が「Slice 1 の宿題 (annotation 必要)」を解消できる。
追加 / 変更ファイル
変更のみ (新規なし):
src/Resource/Page/Product.php,Cart/Item.php,Entry.php,Shopping/Checkout.php—onPost/onGetの全 user-input 引数に@psalm-taint-source input $paramName。be/src/Input/{AddCartItem,Checkout,ConfirmOrder,GetProduct,RegisterCustomer}Input.php— 全 promoted constructor 引数に@psalm-taint-source input(Input の property 経由でアクセスされた場合に flow が立つ)。be/src/Reason/Service/MailerInterface.php—sendOrderConfirmation($order)に@psalm-taint-sink html(実装が email body として render するため)。be/src/Reason/Service/PaymentGatewayInterface.php—checkout()の全 3 引数に@psalm-taint-sink network(custom taint type、外部 service への出口)。be/src/Reason/Service/SessionInterface.php—customerId()return に@psalm-taint-source session(HTTP session 由来、AAA boundary)。
Honest finding (重要) — Be Framework は Psalm から opaque
annotation 追加後も composer psalm-taint は 0 errors のまま。これは「flow が存在しない」 のではなく「Psalm の data-flow analyzer が Be Framework の #[Be] cascade を辿れない」ためである。
具体的な観察:
Shopping/Checkout::onPost($preOrderId)に@psalm-taint-source input $preOrderIdを付与- その値が
new CheckoutInput(preOrderId: $preOrderId)に渡る — ここまでは Psalm が見える ($this->becoming)($input)で metamorphose —BecomingInterface::__invoke()はobjectを返すので、Psalm は次の class がCheckoutPreparedであることを知らないCheckoutPrepared::__construct(#[Input] string $preOrderId, ...)で再度 string を受けるが、Psalm にとってはこの string は別の origin から来た新規変数- 以降
CheckoutSettled/CheckoutCompletedまで同様 — taint chain がBecoming::__invokeで切れる
結果として、PaymentGateway::checkout($preOrderId, ...) の sink には到達するが、源流は「Becoming の object 返り値」 までしか辿れず、Resource onPost の @psalm-taint-source と接続されない。
これは Slice 1 の commit message が予告していた通り:
Psalm’s stock taint sources are PHP superglobals; BEAR’s #[Input] and Be’s Input classes are unknown to it.
Slice 9 で Input class の property にも source annotation を足したが、#[Input] attribute が Psalm にとってただの noop 装飾なので、Being の constructor #[Input] string $preOrderId 経由でも flow は再構築されなかった。
Slice 9 の現状認識 (重要)
composer psalm-taint の green は 「セキュリティ OK」 を意味しない。Slice 1 と同じ honest framing を踏襲する:
- annotation は「boundary contract の文書化」として機能する (Resource onPost = source、Mailer = sink、等)
- 実装変更 (例: Mailer impl が直接
$_GETを読む) などで Psalm が見える範囲で flow ができれば、その時点で taint analysis が flag を上げる - Be Framework chain 経由の flow を audit したい場合、(a) Psalm plugin を書く、(b) per-class explicit propagation を Being constructor に追加する、(c) 別ツール (Phan, Psalm shepherd, または手動 review) を併用する、のいずれかが必要
検証
composer test— 90 tests / 205 assertions green (Slice 8 から変化なし、annotation 追加のみのため)composer psalm— errors なしcomposer psalm-taint— errors なし (前述の理由で、Be Framework chain 内の flow は不可視。annotation は文書化価値あり)
設計上の判断
Command / Query interface は sink を付けない
CartCommandInterface::save(), OrderCommandInterface::register(), 各 *QueryInterface::byXxx() 等は SQL に到達する候補だが、現在の Fake 実装は in-memory map なので「sink がまだ存在しない」。Phase 2 で Doctrine / Ray.MediaQuery 実装が入った時点で @psalm-taint-sink sql を付与する方が、誤検知を作らない。
代わりに HANDOVER の「EC-CUBE-side contract」 セクションで「将来 SQL impl を入れる時の sink ポイント」 として明文化する (本 Slice の積み残しに記載)。
Semantic 値オブジェクトに escape を付けない
Semantic\Email::__construct 等は format-validate するが「html / sql / shell を escape する」 とは厳密に言えない。例えば valid email foo+'<x>@example.com は HTML / SQL コンテキストでは依然危険。Psalm の @psalm-taint-escape は「この sink type を完全に無害化した」 という宣言なので、format-only validation には付けない方が誠実。
network taint type の採用
Psalm 組み込みの taint type には「外部サービスへの出口」 を直接表すものがない (header は HTTP response header、ldap は LDAP query 等)。network を custom type として採用し、PaymentGateway の sink に使う。本コードベース内で意味が閉じていれば custom type も Psalm は track する。
CsrfToken には taint marker を付けない
Slice 8 の CsrfToken::isValid($token) は $token を比較するだけで、それ以上どこへも流さない。Psalm の sink としては「validate 後の比較対象が leak しないか」 が論点になり得るが、hash_equals で局所利用されるだけなので flow が成立しない。annotation を付けても情報量がゼロのため省略。
Slice 9 の振り返り (決定的/非決定的)
- 決定的だった:
- Resource onPost を source としてマークする方針
- Mailer / PaymentGateway を sink としてマークする方針
- SessionInterface::customerId を session source としてマーク
- 非決定的だった (= ユーザー判断要):
- Command/Query interface に sink を付けるかどうか — 「付けない」 を採用 (Phase 2 で実装と同時)
- Semantic 値オブジェクトに escape を付けるかどうか — 「付けない」 を採用 (format validate ≠ context escape)
networktaint type の命名 —networkを採用
- 積み残し:
- Be Framework
#[Be]chain の opacity — 本 Slice の最大の発見。Slice 11 候補 (Psalm plugin or per-class manual propagation) で対応。 - Phase 2 sink 追加 TODO list — 以下の interface には real impl 投入時に
@psalm-taint-sinkを必ず追加すること:CartCommandInterface::save / clearByPreOrderId→sqlOrderCommandInterface::register→sqlCustomerCommandInterface::register→sql*QueryInterface::*(preOrderId / productCode / cartKey が WHERE 句に入る) →sql- 任意の log writer →
log
- EccubeSharedSessionAdapter / EccubeSharedCsrfTokenAdapter —
$_SESSIONを読むので Psalm 組み込みの session source としては既に認識されているはず。明示的な annotation は冗長になるため省略。 - psalm-baseline.xml — 本 Slice では update なし (新規 issue ゼロ)。
- Be Framework
次の Slice (Slice 10 以降)
| 候補 | 性質 | 一行コメント |
|---|---|---|
| (追加検討) Slice 10: 存在オラクル軽減 | 非決定的 | 既存 user に対する 404 / 403 統一 |
| Slice 11: Be Framework Psalm plugin | 非決定的 | Slice 9 で発見した opacity 問題への対策。plugin が #[Be] の chain を辿って flow propagation を行う |
| (追加検討) bear-security-setup skill 適用 | skill bake | Slice 6-7-8 で手動構築した AUTHZ + CSRF + Slice 9 の taint contract を skill 化するレビュー |
| Slice 7.2 / 8.2 統合: EC-CUBE 側 EventListener | Phase 2 入口 | customer_id + _csrf_token の両 mirror を 1 つの Symfony EventListener にまとめる。Phase 2 キックオフ |
Pilot 6-8 — account 系 3 件 (Direct パターン量産 Batch 1)
| 項目 | 内容 |
|---|---|
| 対象 transition | doLogin (Pilot 6) / doActivateCustomer (Pilot 7) / doUpdateCustomer (Pilot 8) |
| パターン | 3 件とも Direct (Input → Final、Being なし) |
| 採用理由 | account 系 transition は単一副作用 (DB 1 write or 0 write) で完結し、Multi-Reason Being や Diamond の必要が無い |
| テスト | 119 passed (Pilot 1-5 既存 90 + Pilot 6-8 新規 29: domain 14 + resource 15), 273 assertions |
| Psalm / psalm-taint | 全 green |
Pilot 6 — doLogin
- Be flow:
LoginInput → CustomerAuthenticated - 既存
FakeCustomerStorageを read-side で利用するため CQRS split を導入:CustomerQueryInterface(findByEmail/findBySecretKey/findById) +FakeCustomerQuery。Order 系の Command/Query split と同じ規約 PasswordHasherInterface::verify()を追加 (password_verify)。Pilot 4 で hash 側だけ実装されていたものを補完- 設計判断: Session-write は Be 層スコープ外。Slice 7.2 contract で「EC-CUBE EventListener が session に customerId を書く」と決めた通り。Be 層は credentials check の proof (= Final) を返すだけ。BEAR resource もそれを body にして返すのみ
- 設計判断: 「unknown email」 と 「wrong password」 は同一
LoginFailedExceptionに集約。user-enumeration を防ぐため - fixture:
customers.jsonにlogin-test@example.comを本物の bcrypt hash 付きで追加 (alice/bob/carol は dummy hash のまま)
Pilot 7 — doActivateCustomer
- Be flow:
ActivateCustomerInput → CustomerActivated - 新規 Semantic:
SecretKey(URL-safe printable, 16-128 chars) - 新規 Exception:
SecretKeyFormatException/SecretKeyNotFoundException CustomerEntityに nullablesecretKeyプロパティ追加 (default null、既存呼び出し不変)FakeCustomerStorage::getBySecretKey+activate(customerId)(idempotent)- 設計判断: 「wrong key」 / 「expired」 / 「already used」 を
SecretKeyNotFoundExceptionに集約 (enumeration 防止と同じ思想) - 設計判断: HTTP は
onPost+ CSRF — email link UX (GET ?secretKey=...) ではなく、確認画面のフォーム submit を経由する。secretKey が CSRF 代替になる議論もあるが、Slice 8 の境界 contract (全 state-changing endpoint で CSRF) を統一的に保つ方を優先
Pilot 8 — doUpdateCustomer
- Be flow:
UpdateCustomerInput → CustomerUpdated - AUTHZ via Session — Pilot 5 F-2 の mass-assignment 教訓を踏襲:
customerIdは Input に含めない。SessionInterface::customerId()を Final が pull - 新規 Exception:
UnauthenticatedException(Pilot 5 のUnauthorizedPreOrderAccessExceptionは「ログイン済みだが他人のもの」、こちらは「未ログイン」と区別) CustomerQueryInterface::findById+CustomerCommandInterface::update追加FakeCustomerStorage::replace— email-rekey (email 変更時に古い key を unset) 対応- 設計判断: パスワード更新は Pilot 8 スコープ外 → Pilot 14
doRequestPasswordResetで扱う - 設計判断: 部分 update — email は required (uniqueness 再 check は変更時のみ)、他は nullable で「null = この field は触らない」
- Semantic への波及:
Name01/Name02を nullable 受容に変更 (early-return on null)。Pilot 4 の register はstring宣言 (non-null) なので影響なし
Batch 1 振り返り (決定的 / 非決定的)
- 決定的だった:
- Direct パターン採用 (3 件とも単一副作用)
- CQRS split の追加 (既存 Order pattern 踏襲)
- AUTHZ via Session の踏襲 (Pilot 5 / Slice 6 と同形)
- CSRF 強制の継承 (Slice 8 contract uniform)
- user-enumeration 回避の例外集約 (login / activate)
- 非決定的だった:
- Pilot 7 で
onPostを選択 (onGetで email link UX を直接踏襲する選択肢を退ける根拠は Slice 8 contract の uniformity) - Pilot 8 で email change を allow するかどうか → ALPS doc に明記されているため include
Name01/Name02を nullable に変えるか、UpdateCustomerInput で別 Semantic を作るか → 「nullable 受容で early-return」 を採用 (validator 数を増やさず Pilot 4 を破壊しない)
- Pilot 7 で
- 積み残し:
- email-link UX のままにする path (Pilot 7
onGetバージョン) — 必要なら別 Slice - password change — Pilot 14 で扱う
findByIdが O(n) scan (storage map indexes by email) — Phase 2 で DB impl にすれば解消
- email-link UX のままにする path (Pilot 7
Pilot 9-11 — cart manipulation 3 件 (量産 Batch 2)
| 項目 | 内容 |
|---|---|
| 対象 transition | goCart (Pilot 9) / doUpdateCartItemQuantity (Pilot 10) / doRemoveCartItem (Pilot 11) |
| パターン | Direct (Pilot 9, 11) / Linear (Pilot 10) |
| テスト | 128 passed (Batch 1 末 119 + Batch 2 新規 9: cart 3 + cart/item PUT 3 + cart/item DELETE 3), 297 assertions |
| Psalm / psalm-taint | 全 green |
Pilot 9 — goCart
- Be flow:
GetCartsInput → CartsFetched - Multi-cart semantics — EC-CUBE は 1 shopping session を saleType 単位で N cart に partition する (
cartKey = {sessionPrefix}_{saleTypeId})。Final は prefix で scan して per-session totals を集計 - 新規:
CartQueryInterface::bySessionPrefix+FakeCartStorage::getBySessionPrefix - safe read のため CSRF / AUTHZ なし。ownership は sessionPrefix cookie で implicit
Pilot 10 — doUpdateCartItemQuantity
- Be flow:
UpdateCartItemQuantityInput → CartItemQuantityReplacing → CartItemQuantityUpdated - Linear pattern (contact-form demo)
- HTTP: PUT /cart/item (idempotent matches PUT)
- Quantity は 置換 (Pilot 2 doAddCartItem は加算)
- Cap 再適用: stock + saleLimit (PurchaseFlow 相当の最小実装)
- 既存 item 必須 — 無ければ
CartItemNotInCartException→ 404
設計上の発見 (G-17): Be Framework chain は class-level fixed
Pilot 10 は本来 Pilot 2 の QuantityAdjusted Being を再利用したかったが、Be Framework の #[Be(NextClass::class)] attribute は Being class 上に置かれるため、下流の宛先が class level で固定される。QuantityAdjusted は #[Be(CartMerged::class)] を持ち、必ず加算 merge へ流れる。
選択肢:
- (A)
QuantityAdjustedに Branching: 動的な宛先選択 → Be Framework は構造的型付けの哲学から外れる - (B) 同形 Being を別名で複製 (
QuantityCapped等): DRY 違反だが構造的に正しい - (C) Input 段で意図を区別し、Being も分ける: 採用 —
CartItemQuantityReplacingを新規作成
これは G-15 (Multi-side-effect Final 判定基準) と同列の重要発見として SKILL.md 候補に記載すべき。Be Framework での「同じ前処理を異なる下流に向ける」 ケースの規約として Input-per-intent + Being-per-shape を踏襲する。
Pilot 11 — doRemoveCartItem
- Be flow:
RemoveCartItemInput → CartItemRemoved - Direct pattern
- HTTP: DELETE /cart/item (idempotent matches DELETE)
- Final が session prefix 配下の全 cart を scan して該当 productCode を含む cart を見つけ、その中の item を除去 → totals 再計算 → 永続化
- 既存 item 必須 — 無ければ
CartItemNotInCartException→ 404 (再削除は idempotent ではなく明示的 404)
Batch 2 振り返り (決定的 / 非決定的)
- 決定的だった:
- HTTP method の REST 規約 (POST → create, PUT → update, DELETE → remove)
- safe read に CSRF/AUTHZ を付けない (Pilot 1 goProduct の踏襲)
- quantity=0 を Quantity Semantic 側で reject、削除は別 endpoint
- 非決定的だった:
- Pilot 9 で空 carts を 200 で返すか 404 にするか → 200 を採用 (「cart 一覧」 が空であることは正常状態)
- Pilot 10 で QuantityAdjusted を再利用するか別 Being を作るか → 別 Being を採用 (G-17)
- Pilot 11 で 再削除を idempotent (200) として swallow するか 404 にするか → 404 を採用 (UI が「カートが空」と「2 度押し」 を区別できる)
- 積み残し:
- 1 つの productCode が複数 cart (異なる saleType) に存在する場合の挙動 — Pilot 11 は最初に見つけた 1 件だけ削除する。EC-CUBE では同 productCode は 1 cart にしか入らないので実用上問題ないが、Phase 2 で全 cart スキャンに変える余地
- PurchaseFlow の本物の再評価 (送料・手数料・割引) は Phase 2
Pilot 13-15 — favorite / contact / password-reset 3 件 (量産 Batch 3)
| 項目 | 内容 |
|---|---|
| 対象 transition | doAddFavorite (Pilot 13) / doSubmitContact (Pilot 15) / doRequestPasswordReset (Pilot 14) |
| パターン | 3 件とも Direct |
| テスト | 141 passed (Batch 2 末 128 + Batch 3 新規 13: favorite 5 + contact 4 + forgot-password 4), 323 assertions |
| Psalm / psalm-taint | 全 green |
Pilot 13 — doAddFavorite
- Be flow:
AddFavoriteInput → FavoriteAdded - AUTHZ via Session (Pilot 8 と同形)
- 重複追加 idempotent — first add = 201, re-add = 200 with
alreadyExisted=true - 新規:
FavoriteStorageInterface(unified Query+Command for v1) +FakeFavoriteStorage+FavoriteEntity - 設計判断: CQRS split は load が demand したときに deferred (Phase 2)
Pilot 15 — doSubmitContact
- Be flow:
SubmitContactInput → ContactSubmitted - Anonymous accessible — AUTHN / AUTHZ なし、CSRF のみ
MailerInterface::sendContactInquiryを追加 (shop + sender 両宛先は impl 内部で fan-out)- 新規 Semantic 4 件:
ContactName01/ContactName02/ContactEmail/ContactContents- 既存 Name01/Name02/Email Semantic と同一ロジックだが、ALPS descriptor 名 (contactName01 等) で参照されるため、Be Framework の per-param-name wiring に合わせて別 class を作成
- 設計判断: 既存 Semantic を再利用するか別 class を作るか → ALPS 名規約 (
contact*) を保つ別 class を採用
Pilot 14 — doRequestPasswordReset
- Be flow:
RequestPasswordResetInput → PasswordResetRequested - Anti-enumeration: 登録済み email / 未登録 email の双方で identical 200 + identical body shape を返す。
issuedflag は Final 内部にのみ存在 (mail 送信を制御)、resource は client に echo しない - Token: 32-char hex (
CustomerIdProviderを re-purpose)、TTL 1 hour (EC-CUBE デフォルト準拠)、latest-wins - 新規:
PasswordResetTokenEntity+PasswordResetTokenStorageInterface+FakePasswordResetTokenStorage MailerInterface::sendPasswordReset(email, resetKey)を追加 (email が html sink)- 設計判断: token を
CustomerEntityに持たせず別 storage に分離 — expiry 管理が cleaner
Pilot 12 — doReorder (本 Batch では deferred)
ALPS doc: 「過去の受注内容をカートに再投入する。在庫切れ商品はスキップ、現在価格を適用」
Deferred の理由:
FinalizedOrderEntityに items が含まれていない (Pilot 5 で order item は別 table と decided)OrderQueryInterfaceにitemsByOrderNoが無い- Fake fixture
var/fake/orders.jsonに items column が無い - これらすべての追加は 2-unit 規模 (Pilot 1 件分 + infrastructure)
次の Batch (Pilot 12 含む) で着手するべき先行作業:
OrderItemEntity新規FinalizedOrderEntity拡張 (items: list) OrderQueryInterface::itemsByOrderNo追加orders.jsonfixture に items 追加 (alice’s history で 2-3 件)- Cart 側との merge ロジック (existing cart があれば追加 / 無ければ新規)
- 在庫切れ skip / 廃番 skip の policy
Batch 3 振り返り (決定的 / 非決定的)
- 決定的だった:
- Anti-enumeration の uniform 200 response (Pilot 14)
- Anonymous-accessible / AUTHN required の区分 (Pilot 15 vs Pilot 13)
- Token を別 storage に分離 (Pilot 14)
- 非決定的だった:
- Pilot 13: idempotent re-add を 200 (alreadyExisted) で返すか silent 201 にするか → 200 を採用 (UI 区別性)
- Pilot 15: Semantic class を新規作成するか既存を再利用するか → 新規作成 (per-param-name wiring 規約)
- Pilot 14: token を
CustomerEntityに持たせるか別 storage にするか → 別 storage (expiry handling cleaner)
- 積み残し:
- Pilot 12 全体 — 上記 deferred 理由参照
doResetPassword(Pilot 14 の対) — token 消費側、別 Pilot でdoRemoveFavorite— Pilot 13 の対、FavoriteStorageInterface::removeは既に実装済みなので軽量- Resource Page で
/favorite/list(お気に入り一覧) — Pilot 13 の query 側を Public にする
Batch 1-3 累計の進捗
| 指標 | Pilot 1-5 末 | Batch 3 末 | 差分 |
|---|---|---|---|
| 移植済み transition 数 | 5 / 137 | 13 / 137 | +8 (Pilot 6, 7, 8, 9, 10, 11, 13, 14, 15 ※ 9 件) |
| Transition 量産率 | 3.6% | 9.5% | +5.9 pt |
| テスト数 | 90 | 141 | +51 |
| パターン実証 | 5 種 | 5 種 (Direct 多発生 + Linear 1 新) | (新規パターンなし、Direct の量産技法を確立) |
実証された pattern instance:
- Direct: Pilot 1, 6, 7, 8, 9, 11, 13, 14, 15 (9 件) — 量産可能、AUTHZ via Session / anti-enumeration / idempotent re-do などの規約が定着
- Linear: contact-form (元) + Pilot 10 (新) —
Input-per-intent + Being-per-shapeの規約を Pilot 10 で確立 (G-17) - Multi-Reason Being: Pilot 4 (CustomerRegistering)
- Diamond-Cascade: Pilot 2 / Pilot 5 (CheckoutPrepared)
- Branching Final: Pilot 3
Wave 1 + Wave 2 — Orchestrated parallel agents (Pilot 12 + 5 新 transition)
指揮スタイル: 単一 agent の直列実行から、worktree-isolated 並列 subagent への切替。1 user turn で 3-4 agent を kick → 完了通知ごとに本ブランチへ cherry-pick → push のループ。
Wave 1 (3 agent 並列、worktree isolation)
| Agent | 対象 | 結果 | テスト追加 |
|---|---|---|---|
| A | Pilot 12 prep (OrderItemEntity infrastructure) |
5fbe6d6 cherry-picked as 3028041 |
+3 (domain) |
| B | doRemoveFavorite (Pilot 13 idempotent inverse) |
4521c6f cherry-picked as c366723 |
+4 (resource) |
| C | doResetPassword (Pilot 14 single-use consumer) |
951038c cherry-picked as 87e0319 |
+11 (4 domain + 7 resource) |
Wave 1 net: +18 tests (141 → 159)、新 transition 2 件 (doRemoveFavorite / doResetPassword) + 1 infra prep。
Wave 2 (4 agent 並列、worktree isolation)
| Agent | 対象 | パターン | 結果 | テスト追加 |
|---|---|---|---|---|
| D | Pilot 12 doReorder |
Diamond-Cascade (loan-application) | 7366c90 cherry-picked as 263c525 |
+11 |
| E | doLogout |
Direct (session-clear by EventListener) | 7f18d89 cherry-picked as 351dcd1 |
+5 |
| F | goMypage |
Direct safe-read (dashboard aggregation) | e7cb6dd cherry-picked as cef5447 |
+4 |
| G | doWithdrawCustomer |
Direct + multi-side-effect | a235ad9 cherry-picked as ab2e674 |
+9 |
Wave 2 net: +29 tests (159 → 188)、新 transition 4 件。
Wave 2 で発見された設計事項
Pilot 12 (Agent D)
- Stage 1 Being
ReorderResolvingが AUTHN/AUTHZ + past-items load + per-item current ProductClass 解決 + cap 適用を 1 段で吸収 (Pilot 5CheckoutPreparedの方針踏襲) Included/SkippedSemantic — Being → Final 間で list payload を運ぶ pattern。MergedCart/PaymentVerification既存パターンの踏襲 (composite-type validate body 空)- Skip-rather-than-fail — 廃番 (ProductClass null) / 在庫切れ (stock=0 & !stockUnlimited) は skip; 数量 over は cap で adjust。EC-CUBE doc の「在庫切れ商品はスキップ、現在価格を適用」 を踏襲
- prep 不足の発見: Wave 1A の Pilot 12 prep は
itemsByOrderNoのみ追加していてbyOrderNo(header lookup) が無かった。Agent D が in-flight で interface 拡張 (storage 側にすでにgetByOrderNoが存在したため最小追加)
Pilot doLogout (Agent E)
- 0-arg Input が Be Framework で動く確認 —
BecomingArgumentsのgetParameters()空 loop は no-op。dummy field fallback は不要だった - Session-clear は EC-CUBE EventListener 側 (Slice 7.2 contract)、Be 層は LoggedOut Final で「処理した」 という proof を返すのみ — Pilot 6 doLogin と同形
goMypage (Agent F)
- dashboard aggregation pattern — 1 Final で複数 Reason (CustomerQuery + OrderQuery + FavoriteStorage) を converge し、shallow projection を組む
recentOrdersは flat projection ({orderNo, total, orderDate, orderStatus})、favoriteCountのみ (full list は出さない)。dashboard scope の規約OrderLimitSemantic 新規 (1-50 cap) —SessionPrefix等の int-typed semantic 規約踏襲
doWithdrawCustomer (Agent G)
- Multi-side-effect Final (Pilot 5 convention): capture-original-email → replace-record → clear-carts → send-mail の strict order
- Dummy email:
withdrawn-{customerId}@example.test(RFC 2606 reserved.testTLD) customerStatus=3= withdrawn を const として publish —FinalizedOrderEntity::STATUS_NEWパターン踏襲- Idempotency short-circuit: 既に status=3 なら mail 再送なし。
cleared=trueは idempotent replay 時も維持 (UI 区別なし)
Cherry-pick 衝突対応
Agent D (Pilot 12) と Agent F (goMypage) は両者 OrderQueryInterface / FakeOrderQuery を拡張 (D: byOrderNo、F: listByCustomer)。git auto-merge が成功 — 異なる method を追加していたため。Wave 設計時の disjoint-files 原則が機能した。
Orchestration の振り返り
- walltime 短縮: Wave 1 (3 agent) は wall ~5min、Wave 2 (4 agent) は wall ~12min。直列なら 7 unit ≒ 1.5-2 倍時間
- briefing の粒度: prep 工程 (Wave 1A) と本実装 (Wave 2 D) を別 wave にしたのは正解。1 agent が両方やると context が膨らみすぎ判断が雑になる
- prep 漏れの発見 (Pilot 12 の
byOrderNo): briefing で「依存している interface methods をすべて列挙する」 工程が抜けていた。Wave 3 以降は briefing 設計時に dependency 表を作成 - 既存 file への複数 agent 書込み: G (CartCommand 拡張) と D (CartCommand 読込) は read/write split で OK、F (OrderQuery 拡張) と D (OrderQuery 拡張) は git auto-merge 任せで OK。3 agent 以上が同 file を write する場合は事前に分担を明示するべき
累計進捗 (Wave 2 末)
| 指標 | Pilot 1-5 末 | Batch 3 末 | Wave 2 末 |
|---|---|---|---|
| 移植 transition | 5 / 137 (3.6%) | 14 / 137 (10.2%) | 20 / 137 (14.6%) |
| Tests | 90 | 141 | 188 |
| Assertions | 205 | 323 | 477 |
実証された pattern instance:
- Direct: 13 件 (Pilot 1, 6, 7, 8, 9, 11, 13, 14, 15, doLogout, goMypage, doWithdrawCustomer, doResetPassword, doRemoveFavorite)
- Linear: 1 件 (Pilot 10)
- Multi-Reason Being: 1 件 (Pilot 4)
- Diamond-Cascade: 3 件 (Pilot 2, 5, 12)
- Branching Final: 1 件 (Pilot 3)
Wave 3 + Wave 4 + Wave 5 — オーケストレーション習熟期
1 turn = 3-4 並列 agent + per-agent cherry-pick + integrated test/psalm verify のループが定常運転に。Wave 3 で 8 transition (3 agent)、Wave 4 で admin 基盤 + 2 transition (1 agent)、Wave 5 で 3 transition (3 agent) を投入。
Wave 3 (8 transition、3 agent 並列)
| Agent | 対象 | 結果 | テスト |
|---|---|---|---|
| H | go* pure renderers (goLogin / goCustomerRegistration / goContactForm / goMypageWithdraw) |
6dba995 |
+6 |
| I | goMypageHistory / goMypageChange (Direct + authenticated) |
e6ac521 |
+13 |
| J | goShopping (Direct aggregation) |
ac1ce6f |
+9 |
Wave 3 net: 188 → 216 (+28 tests)、新 transition 7 件 (goForgotPassword は ALPS 不在のため正当に skip)。
発見
- pure form-info endpoint は Be Framework を使わず BEAR Resource 単体で実装する規約を確立 (H が判断)。
{transitionId, fields, submitTo, csrfToken}のuniform body shape goForgotPasswordが ALPS に無い —doRequestPasswordReset(POST) は存在するが、その入口の form-show transition は ALPS に登録されていない。agent が invent しなかったのは正解 (orchestrator briefing で「skip して報告」 と明示済)PaymentMethodFactoryInterface::available()を Wave 3J が新規追加 (Pilot 5 では single method lookup のみ実装、enumeration は未着手だった)
Wave 4 (admin AAA infrastructure + 2 transition、1 agent)
| 対象 | 結果 | テスト |
|---|---|---|
admin AAA infra + doAdminLogin + doAdminLogout |
b925397 |
+13 |
重要な発見 (G-18): 仕様外 transition の発見規約
agent が ALPS を網羅探索した結果、doAdminLogin / doAdminLogout は alps.json に存在しない ことを発見。Member 系 admin user CRUD (goMemberList / doUpdateMember 等) は存在するが、admin 自身の auth/logout transition は欠落。
採用した対応:
- agent は「conventional な命名 (
doAdminLogin/doAdminLogout) で実装、docblock に ALPS 欠落を明記、orchestrator に報告」 を選択 - orchestrator が事後に alps.json に該当 transition を追記 (
actor-admintag、loginId+passworddescriptor)
G-18 として記録: ALPS と実装の往復は片方向ではない。実装 agent が「ALPS にあるべきだがない」 transition を見つけた場合、agent が ALPS を勝手に編集するのではなく、(a) conventional 名で実装 (b) gap を docblock + return message で報告 (c) orchestrator が ALPS の整合性を取る、の責務分担を採用。
admin AAA infrastructure (新設)
AdminEntity(parallel toCustomerEntity、admin shape:adminId/loginId/passwordHash/name/mailAddress/authority)AdminQueryInterface(findByLoginId/findById) +FakeAdminQuery+FakeAdminStorageAdminSessionInterface(adminId(): ?string、@psalm-taint-source session) +FakeAdminSessionadmins.jsonfixture (3 seed admins、test-adminが本物の bcrypt password)UnauthorizedAdminAccessException/AdminLoginFailedException/LoginIdFormatExceptionLoginIdSemantic
EC-CUBE は admin と customer の二重 firewall モデル — それを mirror。同じ SessionInterface に admin id を相乗りさせず、別 interface に分離した方が AAA boundary が明確になる。
Wave 5 (3 transition、3 agent 並列、admin AUTHZ 量産)
| Agent | 対象 | パターン | 結果 | テスト |
|---|---|---|---|---|
| M | goCustomerList |
Direct + admin AUTHZ + filter search | 31e1d93 |
+11 |
| N | goCustomer |
Direct + admin AUTHZ + aggregation | 1e22b42 |
+7 |
| O | doCreateCustomer |
Multi-Reason Being + admin AUTHZ (Pilot 4 並列) | 0bb3ea0 |
+9 |
Wave 5 net: 229 → 256 (+27 tests)、新 transition 3 件。
発見
- admin AUTHZ pattern が CustomerQuery / OrderQuery / FavoriteStorage を across 透過的に使える — admin が customer entity を読む経路は customer 自身が読む経路と同じ Reason を共有 (AAA は session-side で吸収)。読み専用 Reason 群が role-agnostic に設計されていた効果
- G-17 再確認 — Wave 5O
doCreateCustomerで改めて確認: Pilot 4CustomerRegisteringを再利用すれば DRY だが#[Be(CustomerRegistered)]が class-level fixed なので admin 用 Final へ流せない。Input-per-intent + Being-per-shape を踏襲してAdminCustomerCreatingを別 class 化 - Anti-enumeration ladder — Wave 5N
goCustomerで AUTHZ check (403) → existence check (404) の順序を確立。403 のレスポンスは email を echo back しないことを test で pin
累計進捗 (Wave 5 末)
| 指標 | Pilot 1-5 | Pilot 15 末 | Wave 2 末 | Wave 5 末 |
|---|---|---|---|---|
| 移植 transition | 5 (3.6%) | 14 (10.2%) | 20 (14.6%) | 30 (21.9%) |
| Tests | 90 | 141 | 188 | 256 |
| Assertions | 205 | 323 | 477 | 798 |
実証された pattern instance:
- Direct: 19 件 (累計)
- Linear: 1 件 (Pilot 10)
- Multi-Reason Being: 2 件 (Pilot 4 + Wave 5O
doCreateCustomer) - Diamond-Cascade: 3 件 (Pilot 2, 5, 12)
- Branching Final: 1 件 (Pilot 3)
新 skill gap
- G-18: ALPS 不在 transition の発見と編集責務 — 上記
- G-19: admin AAA は parallel firewall として独立 interface 化する — Wave 4 で発見。
SessionInterface(customer) /AdminSessionInterface(admin) を分離することで、(a) 一方の null check で他方を無効化する事故を防げる (b) audit log で role 判別が明示的になる (c) BEAR resource でCode::UNAUTHORIZED(顧客) vsCode::FORBIDDEN(admin-only endpoint だが顧客 logged-in、role mismatch) を明確に分けられる
alps.json update
orchestrator が事後追記:
doAdminLogin(unsafe、actor-admintag、loginId+passworddescriptor)doAdminLogout(idempotent、actor-admintag、no descriptor)
これで Wave 4 で実装した 2 transition が ALPS-traceable に。alps.json の JSON validity も php -r json_decode で確認済。
Wave 6 — domain 拡張 + pair completion (7 transition、4 agent 並列)
| Agent | 対象 | 内訳 | 結果 | テスト |
|---|---|---|---|---|
| P | customer address book (4 transition) | goCustomerAddressList / doCreateCustomerAddress / doUpdateCustomerAddress / doDeleteCustomerAddress — 単一 agent で AddressEntity infrastructure 込み |
30065aa |
+26 |
| Q | goFavoriteList |
Pilot 13 read pair | dc660f2 |
+6 |
| R | goOrderHistory |
customer 全件 + pagination | bea5948 |
+7 |
| S | doDeleteCustomer |
admin soft-delete、Wave 5O pair | 1b31d91 |
+11 |
Wave 6 net: 256 → 306 (+50 tests)、新 transition 7 件。
Wave 6 で発見された設計事項
G-20: Singleton storage と cross-session 切替テスト
Wave 6P で発見:
- AUTHZ test (Alice の address を Bob が編集しようとして 403) で session を rebind すると、各 Injector が独立した
FakeAddressStoragesingleton を持つため、Alice の write が Bob の view に見えない - 解決: テスト setUp で
$storage = new FakeAddressStorage(); $module->bind(AddressStorageInterface::class)->toInstance($storage); $module->bind(FakeAddressStorage::class)->toInstance($storage);を rebind 時にも維持する - これは Pilot 5 で発見した G-14 (Ray.Di binding gotcha) の cross-session 切替版。今後の AUTHZ test で session rebind パターンを使うケースに適用
G-21: idempotent DELETE の 2 つのスタイル
Wave 6 で 2 つの DELETE 実装が並存:
- Pilot 11 / 13 / 6S
doRemoveFavorite: silent idempotent — 不在の item を削除しても 200 +alreadyAbsent: true、UI が flag で区別 - Pilot 11
doRemoveCartItem/ Wave 6PdoDeleteCustomerAddress: 404 on miss — 認証済み caller には精密 feedback、idempotent 性は「persisted state は同じ」 で保たれる
規約: 一般的な “rare-but-OK” path (お気に入りを 2 回押した) は silent、本当に変更を期待する path (cart や住所) は 404。Wave 6P がこの規約を明文化。
G-22: pagination の Semantic 命名
Wave 6R goOrderHistory で、既存 Limit Semantic (1-50) を再利用するか別 HistoryLimit Semantic を作るかが論点に:
- 既存
OrderLimit(1-50) は dashboard / grid 用、Limit(1-50) は admin search 用、HistoryLimit(1-200) は full-history 用 - Be Framework の per-param-name wiring 規約により、param 名で Semantic が選択される → 同じ「数値の cap」 でも context によって別 class
- DRY 違反だが、cap range が context-specific であるべきという ALPS 思想とも整合
- Wave 3
OrderLimit設定時にこの分岐が起きていれば DRY の議論が浮上していたが、当時 Wave 6R を想定していなかった
累計進捗 (Wave 6 末)
| 指標 | Pilot 1-5 | Pilot 15 末 | Wave 2 末 | Wave 5 末 | Wave 6 末 |
|---|---|---|---|---|---|
| 移植 transition | 5 (3.6%) | 14 (10.2%) | 20 (14.6%) | 30 (21.9%) | 37 (27.0%) |
| Tests | 90 | 141 | 188 | 256 | 306 |
| Assertions | 205 | 323 | 477 | 798 | 962 |
累計 skill gap 一覧 (このセッションで発見)
| ID | 内容 | 発見 wave |
|---|---|---|
| G-14 | Ray.Di bind(Iface)->to(Impl) は singleton scope を consult しない |
Pilot 5 (前 session) |
| G-15 | Multi-side-effect Final (Complex Convergence) の判定基準 | Pilot 5 (前 session) |
| G-16 | server-derived Semantic 登録漏れ | Pilot 5 (前 session) |
| G-17 | Be Framework chain は #[Be] で class-level fixed → Input-per-intent + Being-per-shape |
Pilot 10 |
| G-18 | ALPS 不在 transition の発見規約 — agent が conventional 名で実装 + orchestrator が ALPS 整合 | Wave 4 |
| G-19 | admin AAA は parallel firewall (SessionInterface / AdminSessionInterface 分離) |
Wave 4 |
| G-20 | cross-session rebind 時の singleton storage 共有パターン | Wave 6P |
| G-21 | idempotent DELETE の “silent” vs “404 on miss” 規約 | Wave 6P |
| G-22 | pagination Semantic は context-specific (Limit / OrderLimit / HistoryLimit) |
Wave 6R |
これら G-17 以降は be-framework-skills repo / alps-skills repo へ contribute する候補 として整理可能。Wave 7 (SKILL bake) で実施候補。
orchestration マトリクスの定常運転
セッション後半 (Wave 1-6) の運用パターン:
- orchestrator が wave 設計時に dependency 表を作成 (Wave 4 で漏れて Wave 5 brief で補完)
- agent 並列度は 3-4 が安全圏 (Wave 2 で 4 agent、Wave 5/6 で 3-4 agent)
- 各 agent には briefing で「STOP and report」 ルートを明示 → S1-S7 stop condition 相当の自律判断を委譲
- 完了通知ごとに orchestrator が cherry-pick + integrated test/psalm verify → push
- wave 完了時に HANDOVER 追記 + HOW_TO_CONTINUE 反映 + PR body 更新 (orchestrator 専任)
walltime 効率: 直列なら累計 12-15 unit が、6 wave × ~5-10 min walls で着地。約 60-90 分セッション内で 32 新 transition + 9 skill gap 発見。
Wave 7 — SKILL bake + admin order + guest checkout (3 agent 並列)
3 agent 並列のうち 1 は docs-only (X)、2 はコード生産 (Y, W)。docs と code を独立 agent に分けることで、SKILL bake (G-NN の externalize) が code 生産と非競合に走る運用を確立。
| Agent | 対象 | 結果 | テスト |
|---|---|---|---|
| X | SKILL bake — docs/skills/G-14 〜 G-22 の 9 件 + index.md |
2241185 |
0 (docs only) |
| Y | admin order management 4 件 (goOrderList / goOrder / doUpdateOrder / doUpdateOrderStatus) |
a59485a |
+38 |
| W | guest checkout entry 2 件 (goShoppingNonMember / doSubmitNonMember) |
f326e12 |
+6 |
Wave 7 net: 306 → 350 (+44 tests)、新 transition 6 件、新 docs 10 件。
Wave 7 で発見された設計事項
Y (admin order management) で確立した規約
- status constants を
FinalizedOrderEntityに EC-CUBEdtb_order_status(1, 3-9) verbatim 採用。STATUS_NEW=1、STATUS_CANCEL=3、STATUS_IN_PROGRESS=4、STATUS_DELIVERED=5、STATUS_PAID=6、STATUS_PENDING=7、STATUS_PROCESSING=8、STATUS_RETURNED=9。値 2 は EC-CUBE 4.x で削除されたため除外 - mass-assignment safety の admin variant —
doUpdateOrderの editable field はdiscount,charge,usePointのみに narrow。customerId/total/orderStatus/orderDate/paymentDateは body から書けない (round-trip verbatim) - status flip は独立 sub-resource —
src/Resource/Page/Admin/OrderStatus.phpをOrder.phpから分離。workflow significant な操作 (cancel = stock 戻し、ship = point 付与) は将来別 chain になる想定 - Semantic widening (nullable 化) —
Charge/Discount/UsePointSemantics をint|nullに変更。partial-update Input の null 値を受容 (Pilot 8Name01/Name02の nullable 化 と同パターン)
W (guest checkout) のスコープ判断
- ALPS の
goShoppingNonMember/doSubmitNonMemberは form entry only として実装、guest checkout の AUTHZ 緩和 (Pilot 5 doCheckout を session-less で通す) は Phase 2 として明示 defer doSubmitNonMemberFinal はpreOrderIdをCustomerIdProvider(32-char hex) で synthesize して返すが、Cart や PreOrder には書かない stub。Final docblock で Phase 2 gap を明文化- 32-char hex (CustomerIdProvider) vs 40-char hex (
PreOrderIdSemantic) の format 不一致も docblock で記録 — Phase 2 でPreOrderIdProviderを独立 interface 化する余地
X (SKILL bake) の構造
- 10 ファイル (
index.md+ G-14 〜 G-22) をdocs/skills/配下に配置 - 各 G-NN file は self-contained — future engineer がプロジェクト context 無しに読める
- 構造:
Context → Problem → Solution → Code example → Anti-pattern → Where this matters → Related - 各 file 90-145 行、200 行 cap 内
- 上流 contribution 候補振り分け: G-14/15/16/17/22 →
be-framework-skills、G-18/19 →alps-skills、G-20/21 → either - agent flag: 旧 HANDOVER の G-16 は別概念 (side-effect ordering / partial-commit window) を含んでいた。Wave 7 brief は「server-derived Semantic registration NOTICE」 という後者解釈を採用。partial-commit window topic は G-15 末尾で言及のみ、独立 G-NN は未起票 (Phase 2 候補)
累計進捗 (Wave 7 末 = セッション最終)
| 指標 | session 開始時 | Wave 7 末 | 増分 |
|---|---|---|---|
| 移植 transition | 5 / 137 (3.6%) | 45 / 137 (32.8%) | +40 |
| Tests | 90 | 350 | +260 |
| Assertions | 205 | 1120 | +915 |
| Skill gap 発見 | 3 (G-14/15/16) | 9 (G-14 〜 G-22) | +6 |
| Skill docs (externalized) | 0 | 10 (docs/skills/) |
+10 |
| Branch commit 数 | 10 (pre-session) | 約 50 | +40 |
| Wave (parallel orchestration) | 0 | 7 wave | 構造化定着 |
Session summary
前半 (Pilot 6-15): 直列 agent (主に自分自身が driver)、9 Pilot を 6 commit 投入。Pilot 12 を一度 defer。 中盤 (Wave 1-2): orchestrator pattern 確立。worktree-isolated parallel subagent への切替。Pilot 12 prep + Pilot 12 本実装を別 wave に分割し成功。 後半 (Wave 3-7): 3-4 agent 並列が定常運転。各 wave 完了時 cherry-pick + integrated test/psalm verify + push の reflexes が定着。SKILL bake で外部資産化まで到達。
実証された pattern instance (累計):
- Direct: 30+ 件
- Linear: 1 件 (Pilot 10)
- Multi-Reason Being: 2 件 (Pilot 4, Wave 5O)
- Diamond-Cascade: 3 件 (Pilot 2, 5, 12)
- Branching Final: 1 件 (Pilot 3)
Phase A の transition 量産という当初目標 (Pilot 1-5 = 5/137 = 3.6%) に対し、1 セッションで 32.8% まで到達。残り 92 transition は大半が admin tooling (product CRUD / category / plugin / layout / CSV export-import 等) + customer の細部 (point / address detail / shipping date 等) で、新規 pattern 発見の余地は限定的。Phase 2 (Fake → real DB / Mailer / 本物の AUTHZ 統合) への移行 readiness が今 session で大幅に向上。
Wave 8 + Wave 9 — 100% transition coverage
45 → 139 transitions (32.8% → 100%) in two big parallel waves.
Wave 8 (5 agents): 49 transitions
| Agent | 対象 | 結果 | テスト |
|---|---|---|---|
| α | admin product CRUD (8) | c4452b1 |
+67 |
| β | category + className + classCategory (15) | c2fdef2 |
+81 |
| γ | admin member + login history (7 + skip 1) | 7047f93 |
+52 |
| δ | help + top + shopping renderers (11) | 4196f37 |
+11 |
| ε | plugin + base info + mail template + trade law (8) | c4c831f |
+40 |
Wave 8 net: 350 → 601 (+251 tests), 45 → 94 transitions.
Wave 8 で発見された運用課題
G-23: 並列 agent で複数 file 同時編集の cherry-pick 衝突
- 5 agent が
AppModule.phpを編集 → 4 件で textual conflict (auto-merge fail) - 各 agent が自分専用の anchor comment (
// Wave 8α:等) を持っても、imports が他 agent の imports と隣接して衝突 - 規約: AppModule 編集を要する複数 agent を並列起動する場合、orchestrator が cherry-pick 時に conflict resolution を都度実施 (今回 4 件解決済)
- 代替案: AppModule を分割 (CustomerModule / AdminModule / CmsModule) → Phase 2 検討
G-24: 同名 Semantic class の意図せぬ重複作成
- Wave 8α と Wave 8β が両方
SortNoSemantic を作成 (前者 0+、後者 0-9999) - Cherry-pick 時に file-level conflict (
<add/add>)、SortNoFormatException も同様 - Orchestrator が手動 merge — 0-9999 range を採用 (より制約的)
Stale autoload cache の罠
- Cherry-pick 直後の
composer testが AppModule の最新 class を見つけられずUnbounderror 多発 (59 件) →composer dump-autoloadで解消 - Wave 9 以降は cherry-pick 後に常に
dump-autoloadを実行
Wave 9 (4 agents): 45 transitions
| Agent | 対象 | 結果 | テスト |
|---|---|---|---|
| ζ | CMS Page+News+Block+Layout+Tag+Template (20) | 50fb50f |
+39 |
| η | Order admin extras (9) | c3414c3 |
+31 |
| θ | Payment+Delivery+TaxRule (11) | c3414c3 |
+26 |
| ι | misc + goodTraded skip (5 + 1 not-a-transition) | 60633b8 |
+12 |
Wave 9 net: 601 → 709 (+108 tests), 94 → 139 transitions。
Wave 9 で確定した最終事項
goodTradedは ALPS data descriptor (BaseInfo entity field “取扱商品”)、transition でないと Agent ι が確認 → 139/139 全 transition 移植が完了とみなすdoUpdateTaxRuleは ALPS に存在せず (TaxRule は create / list / delete のみ) → Agent θ が delete+create のみ実装- 全 Wave 8-9 で stub / 部分実装になった件 (Phase 2 deferral): doImportProductCsv / doImportCategoryCsv / doImportShippingCsv / doInstallPlugin / goExportOrderPdf / doCreateOrder / doUpdateCsv
累計進捗 (Wave 9 末 = セッション最終最終)
| 指標 | 開始時 | Wave 5 末 | Wave 7 末 | Wave 9 末 (最終) |
|---|---|---|---|---|
| 移植 transition | 5 / 140 (3.6%) | 30 (21.4%) | 45 (32.1%) | 139 / 139 (100%) |
| Tests | 90 | 256 | 350 | 709 |
| Assertions | 205 | 798 | 1120 | 2012 |
| Skill gap 発見 | 3 | 7 | 9 | 11 (G-14 〜 G-24) |
| Skill docs externalized | 0 | 0 | 10 | 10 |
| Wave (parallel orchestration) | 0 | 5 | 7 | 9 |
実証された pattern instance (累計):
- Direct: 70+ 件 (admin CRUD 系で多発生)
- Linear: 1 件 (Pilot 10)
- Multi-Reason Being: 3 件 (Pilot 4, Wave 5O, Wave 8α
doCreateProduct) - Diamond-Cascade: 3 件 (Pilot 2, 5, 12)
- Branching Final: 1 件 (Pilot 3)
構築のフェーズ移行
Phase A (transition 量産) はこのセッションで完了。次の Phase 2 の主作業:
- Fake → real persistence: 全
Fake*Storageを Doctrine / Ray.MediaQuery 等に置換 - Fake → real services:
FakeMailer→ EC-CUBE MailService,FakePaymentGateway→ 本物 PSP,FakePurchaseFlow→ EC-CUBE Service - Slice 7.2 / 8.2: EC-CUBE 側 EventListener で session / CSRF mirror
- stub 解消: 上記 Wave 8-9 で stub になった 7 件の本物実装
- Slice 11: Be Framework Psalm plugin (chain opacity 解消)
Session 総括
1 session で 134 transition 移植 + 9 + 2 (G-23/24) skill gap 発見 + 10 docs externalized + 9 wave parallel orchestration を達成。orchestrator + worktree-isolated parallel subagent pattern が大量並列実装の standard workflow として定着した。
Phase 2 — Fake → SQL persistence (Fake ストレージの本番 DB 化)
Phase A は全ストレージを Fake*Storage(in-memory)で実装していた。Phase 2 は
全 34 ストレージインターフェースを SQL 実装(MariaDB / MySQL)へ移植し、
本番 context のバインディングを Sql 側へ切り替えた。サブフェーズ 2a / 2b / 2c で進行。
移植の現状(レイヤ別マトリクス)は
docs/migration-status.mdが正。本セクションは構築プロセスの記録。
スコープと成果
- 2a — SQL スモーク + フレームワーク確立(
b5eb4e5〜fd96242)- EC-CUBE 4.3 の MySQL スキーマを
sql/schema/ec-cube-4.3-mysql-mysqldump.sql(65 テーブル)にダンプ(初期; 後にbemart-schema.sqlへ置換 — 下記注記参照) - BeMart Entity ↔ EC-CUBE テーブルの差分レポート
sql/diff/entity-vs-eccube.mdを作成(8 grade-A 1:1 / 8 grade-B JOIN / 5 grade-C スキーマ差) - 最初の Sql 実装
SqlCustomerQuery+ SQL テストフレームワーク(be/tests/Sql/)を確立 - goCustomer end-to-end・Cart family(
SqlCartQuery/SqlCartCommand)まで縦に通す - Step 5 で ALPS-first ワークフローへ retrofit(SQL 実装から逆に始めていた初期順序を是正)
- EC-CUBE 4.3 の MySQL スキーマを
- 2b — 本体移植(約 28 ストレージ)(
f6f22ee〜9a9c89b)- 各ストレージを Phase A(厳密移植の field alignment)+ Phase B(Sql* 実装 + hypermedia テスト) のペアで移植
- Address / Tag / BaseInfo / TaxRule / News / Page / Block / ClassName / ClassCategory / Category / Layout / Template / LoginHistory / PaymentMethodAdmin / TradeLaw / CustomerCommand / OrderCommand / ShippingAddress / ProductClassQuery / CsvColumnConfig / Plugin / PasswordResetToken / Delivery / MailTemplate / Product 等
- 2c — 本番カットオーバー(
f128ba6,6ed334d)src/Module/SqlModule.phpが prod context で SQL Reason をバインド(Fake → SQL の本番切替)- 再現可能な本番 DB セットアップ:
mtb_*マスタ seed(22 テーブル / 395 行、sql/seed/mtb-master.sql)+sql/setup-db.sh
主要な決定
- G-23 スキル — hypermedia テストが移植契約(
a9dcdd7、docs/skills/G-23-*.md)。ストレージを差し替えても、ResourceInterface::get(...)end-to-end の hypermedia テストが「同じ表現が出る」ことを保証する。Fake → SQL 移植の合否はこのテストで判定する、というのが Phase 2 全体の運用契約。原則エッセイdocs/methodology/hypermedia-test-principle.mdも併設。 - 厳密移植の field alignment — Sql 化の前段(Phase A)で、BeMart Entity が EC-CUBE スキーマに無いフィールド(
sortNo,feeBase,body/htmlBody,mailAddress等)を持っている箇所を先に削る。EC-CUBE 完全移植を優先し、Entity をスキーマに合わせる。 - SQL 実装は素の prepared statement —
be/src/Reason/Query/Sql*.phpは Doctrine を使わず PDO の prepared statement のみ。Be の Reason 層の framework-agnostic 性を保つ。 - テストは 3 面(
sql/README.md参照)— storage unit(Injector 無し)/ Final-direct integration / Resource hypermedia。bemart-sqlテストスイートは毎回eccubedb_testを drop + 再作成し、各テストはトランザクション内で rollback。DATABASE_URL未設定なら clean skip。 - スキーマロードの FK 回避 — bare
CREATE TABLE+ 跨テーブル FK を持つが pragma が無いため、SET FOREIGN_KEY_CHECKS=0/1でラップしてロードする(setup-db.shとbe/tests/Sql/bootstrap.phpで共通)。
現行スキーマファイルについての注記(license-cleanup ブランチ): Phase 2a で使っていた EC-CUBE mysqldump 由来の
sql/schema/ec-cube-4.3-mysql-mysqldump.sqlおよびsql/schema/ec-cube-4.3-mysql.sqlは、著作権上の懸念からsql/schema/bemart-schema.sql(sql/diff/entity-vs-eccube.mdの構造情報を元に first principles から著作した 65 テーブル定義)へ置き換えた。setup-db.shのSCHEMA_FILE変数、be/tests/Sql/bootstrap.phpのロードパス、および各ドキュメントのファイル名参照はすべてbemart-schema.sqlを指すよう更新済み。
積み残し
dtb_*運用データ(実顧客 / 注文 / 商品)の本番移行はsetup-db.shの対象外 — 別の運用作業- Phase A の stub 7 件(
doImportProductCsv等)は Phase 2 では未解消のまま Phase 3 以降へ持ち越し - セッション / CSRF アダプタは本番 cookie/JWT 化を deferred(テストでは Fake セッションのまま)
Phase 3 — HTML presentation layer (HTML プレゼンテーション層)
Phase A / Phase 2 までの BeMart は JSON リソースのみ。Phase 3 は BEAR.Sunday リソースを HTML としてレンダリングし、EC-CUBE のストアフロント画面を再現するフェーズ。
移植の現状・残作業 punch-list は
docs/migration-status.mdが正。本セクションは構築プロセスの記録。
スコープと成果
- Twig レンダリングの配線(
762a739〜)madapaja/twig-moduleを導入、src/Module/HtmlModule.phpでAPP_CONTEXT=htmlの HTML context を定義BeMartTwigExtension(asset/url/path/price等 EC-CUBE Twig ヘルパの再実装)
- ストアフロント全テンプレート移植(7 wave、約 40 画面)(
1507dc2〜46b2a08)var/templates/に EC-CUBE 4.3defaultテーマ Twig の忠実な移植を配置(新規マークアップではなく port)- Top / ProductList / Product detail / Cart / Mypage クラスタ 9 画面 / Shopping(checkout)クラスタ 9 画面 / Entry / Contact / Forgot / Help 5 画面 等
- 共有レイアウト
base.html.twigは EC-CUBEdefault_frame.twigの port
- レンダー差分忠実性テスト — 各ページに
tests/Resource/<Page>HtmlRenderTest.php。EC-CUBE の実テンプレート(gitignore された 4.3 クローン)をスタブ Twig env でレンダリングし、BeMart の移植テンプレートと同じ論理データで差分を取る。差分行は説明付き residual allowlist に限る(allowlist が honesty metric)。 - フォームページ —
ray/web-form-module採用(5a95435Login pilot)。読み取り専用データページとは別に、<input>を持ち POST を受けるページ(Login / Entry / Contact / Forgot)用のレシピを確立。 - ALPS 監査 + 是正(
8d93500,f01e1ae、docs/phases/alps-audit-phase3.md)。Phase 2b で多くのディスクリプタが BeMart 実装 Entity から逆生成(back-form)された疑いを 2 軸で監査し、Favorite の再タグ + 5 遷移の追加を実施。
主要な決定
- HTML は EC-CUBE テンプレートの port であって新規マークアップではない — ALPS は意図的にプレゼンテーションを持たない。HTML を ALPS で採点すると、プレゼンテーションについて沈黙している spec で採点することになる。よって忠実性の基準は EC-CUBE 自身の
defaultテーマテンプレート。詳細はvar/templates/README.md。 - バリデーション権限は Be Framework に残す —
ray/web-form-moduleの役割はフォーム定義 + HTML レンダリング + 再表示(repopulation)に厳密に限定。業務ルールを Aura.Filter に複製しない(spec から drift する)。#[FormValidation]アスペクトは不使用 — Becoming chain が verdict を持つ。 - enrichment backlog — データページの中に、リソース本体(
$ro->body)が薄すぎて EC-CUBE テンプレートを忠実に port できないものがある。これらは Cart スタイルの再導出(ALPS から再導出 → Entity/SQL/Fake を enrich → テンプレート配線)が必要。Cart はパイロットとして完了(a44f296/9d06ec3)、Mypage History もパイロット完了(a31f8d8/3c1b03d)。残: Shopping confirm/complete・Mypage ダッシュボード・Favorite・Address・Contact。 - Twig コンパイルキャッシュ — TwigModule は
var/tmp/<context>/twigにコンパイル結果を残しauto_reloadoff。テンプレート編集後はrm -rf var/tmp/html/twigで clear が必要。
積み残し
- Admin HTML Tier-2(残 約 28 テンプレート) — Tier-1(77 ページ中 34、list/data + 単純 CRUD)は 8 section-wave で完了(下記「Admin HTML — section-wave 並列移植」参照)。その後 4 つの Tier-2 wave で 15 ページを回収(下記「Admin HTML Tier-2 — section ごとの回収」参照)。残る Tier-2 は重量エディタと、action-only リソースに
onGetが無く新規リソース=be/srcドメイン層追加を要するページ群。 - enrichment backlog の残 5 件(上記)
Block/*ウィジェットテンプレート — ヘッダ/フッタ/カート/ログイン/検索のブロック領域は今は EC-CUBE ランタイム residual のまま。ウィジェットレンダリングのサブステップが必要(Block は ALPS で意図的に未モデル化)。Phase 3 中の ALPS 是正で追加した 5 遷移(→ 解消済み。5 遷移すべてドメイン実装完了(下記「Phase-3 是正遷移のドメイン実装完了」参照)。domain coverage は 144/144。doSortNoMove等)はbe/srcにドメイン実装が無い
Admin HTML — section-wave 並列移植(Tier-1 完了)
ストアフロント完了後、admin テーマ(EC-CUBE template/admin/)の移植に着手した。
- admin レシピ確立 + News pilot(
f91e10f)—admin-base.html.twig(admin テーマdefault_frame.twigの port。左サイドバーc-mainNavArea+ ヘッダc-headerBar)、EcCubeAdminStubLoader(admin frame/nav を実レンダリングし他を空 stub)、admin 認証 context(render テストがAdminSessionInterfaceを seeded admin id に rebind)を整備。 News list/edit をパイロット移植。 - per-section ja-message split(
1e91e92)— admintransキーを section ごとに 分割。EcCubeStub::jaMessages()は storefront baseline として凍結、AdminJaMessagesが共有 chrome(frame + nav)、各 section は自前のAdmin/<Section>JaMessages.phpを 持つ。これにより section-wave が共有ファイル衝突なしで並列実行可能になった。 - Customer wave(
da48413)— Customer list/edit。 - section-wave 8 本(per-section ja-split を土台に並列実行)—
- バッチ 1(4 並列): Product(
64e5e03)/ Setting-System(f65279b)/ Content(855c412)/ Top-level(dff64ca) - バッチ 2(3 並列): Setting-Shop(
3b3b42f/1b122af/974b233)/ Store(9261b8e/3acfc6a)/ Order(5a112a1) - 計 34 admin ページ移植、テスト 1656 → 1734。
- バッチ 1(4 並列): Product(
Tier-1 / Tier-2 の切り分け — 各 section-wave は「BEAR リソースが既に GET を提供する ページ(list/data + 単純 CRUD)」= Tier-1 を移植し、以下を Tier-2 として defer した:
- 重量エディタ —
Order/edit(約 1057 行)/Product/product(約 932 行)/Product/product_class(約 448 行マトリクス)/Order/shipping(約 709 行) - 新規リソースが必要なページ — action-only リソース(POST/CSV/PDF)に
onGetが 無いページ群。Store の plugin install/search/confirm 系、Order の mail 系、 Setting/Shop の payment_edit/delivery_edit/calendar 系、Setting/System の authority/system/log/masterdata/security。これらはbe/srcドメイン層 (Input/Final/body-shape)の追加を伴うため、テンプレ移植 wave とは別種の作業。
Tier-2 はこの時点で 77 ページ中 43 ページが残っていた(その後すべて回収済み —
次節「Admin HTML Tier-2 — section ごとの回収(完了)」参照)。section 別の履歴は
docs/phases/admin-fanout-plan.md と var/templates/README.md「Fan-out status」が正。
並列オーケストレーションの教訓 — バッチ 1 の 2 agent(Content / Top-level)が
アカウントの session limit で commit 直前にカットオフされた。両者の WIP は完成・green
だったため手動 salvage で 2 commit に分割して回収(855c412 / dff64ca)。バッチ 2 では
各 agent にページ単位の逐次 commitを指示し、カットオフ耐性を確保した。
Admin HTML Tier-2 — section ごとの回収(完了)
Tier-1 完了後、defer した Tier-2 を section 単位で回収。Tier-2 は
テンプレ移植ではなく「新規 GET リソース/action-only リソースへの onGet 追加/
be/src body-shape」を伴うリソース生成作業(docs/migration-status.md の punch-list 1 参照)。
- flow-manage-system Tier-2 wave(
37c80fb)— 6 ページ(authority / system / log / masterdata / security / two_factor_auth_edit)。5 新規 GET リソース +AuthorityRole::onGet()- 3
<Name>Form+AdminMasterRegistrybody-shape。worked example として最も大規模。
- 3
- Customer delivery-edit(
f872819)— 1 ページ。最小例: 新規 GET リソース 1 +<Name>Form1。Customer section 完了。 - Setting/Shop Tier-2 wave(
0a3724a)— 5 ページ(calendar / csv / mail-template / order_status / tradelaw)。新規 GET リソース 1(Calendar)+ action-only リソース 4 へのonGet追加 + 5<Name>Form。両パターンが同居。 - Setting/Shop edit-page wave(
0b54dee)— 3 ページ(payment_edit / delivery_edit / shop_master)。action-onlyPayment/DeliveryリソースへのonGetエディタ追加 (マスタ一覧 fetch が AUTHZ ゲートを兼ねる)+BaseInfo.onGetへの shop-master フォーム 追加 + 3<Name>Form。Setting/Shop section 完了。 - Order Tier-2 wave(
2f59bb3/4c7c4a1/42214a3/2a45b43/30c97c6/a455281)— 6 ページ(edit / shipping / mail / mail_confirm / order_pdf / csv_shipping)。大型 マルチパネルエディタ(edit~1057L・shipping~709L)をページ単位逐次 commit で移植。 - Product Tier-2 wave(
4eb93f3/a08f38f/9ca00d6/0296306)— 7 ページ (product ~932L / product_class ~448L / category / csv ×4)。 - Store template_add(
571dd5b)— 1 ページ。Store section の残りは plugin install/search 系のみ(移植対象外)。
回収済み計 29 ページ → admin HTML は 77 ページ中 63 ページ移植。残 14 ページは Store/Plugin の install/search サブツリーのみ — プラグインは移植対象外のため、 スコープ内の admin HTML 移植は完了。
Phase 3 storefront — 仕上げ
- Block ウィジェット(
f3df0d4)—logo/footerの 2 ウィジェットをvar/templates/Block/に移植。残る Block 領域(cart/login/search)は EC-CUBE ランタイム残差のまま(Block は ALPS 非モデル化)。 - Shopping confirm/complete エンリッチ(
1177e0d/2f8d17a)— 薄かった resource body を EC-CUBE から再導出し、テンプレートに配線。 - fidelity-test 修正(
5d9e6ba)— Cart/Confirm の render-diff を EC-CUBE 4.3.1 実体に合わせて是正。
Phase B — セキュリティ・本番化
- 静的アセット配備(
a002097)— EC-CUBE 4.3 のdefault+adminテーマの 静的アセットをpublic/に配備。served URL(/assets・/template/admin/assets・/bundle)を忠実にミラー。 - HTTP ルーター(
53e587e/39f1117)—config/aura-routes.phpの Aura route map(EC-CUBE ルート名 ↔ URL パス ↔ リソース URI の extras)で定義。public/index.phpは BEAR.SundayRouterInterface経由でRouterMatchを dispatch し、 missing resource 404 / method 405 は BEAR\Resource に委譲。BeMartTwigExtension::url()/path()は Aura.Generator 経由で解決。 - render-diff スタブのアセットパッケージ対応(
16e8c9d)— EC-CUBE のasset(path, package)パッケージマップ(admin→/template/admin/・bundle→/bundle/)をEcCubeAssetStub経由で両サイド同一に評価。
Phase 3 現在のテスト規模
vendor/bin/phpunit → 1893 tests / 4002 assertions。非 SQL スイート
(--testsuite bemart,bemart-be)は DB 無しで全 green。bemart-sql スイートは
ローカル MariaDB が必要で、無い場合 745 件 skip + prod-DB コンテキスト 3 件が
fail(既知・MariaDB 依存)。正確な現在値は docs/migration-status.md を参照。
Phase-3 是正遷移のドメイン実装完了
Phase 3 の ALPS 監査(8d93500/f01e1ae)で追加した 5 遷移は当初 ALPS-only
(be/src ドメイン実装なし)だった。その後 4 遷移(doSortNoMove /
doToggleVisible / doUpdateTrackingNumber / doSendShippingNotifyMail)が
実装され、最後に残った doResendActivationMail を実装して
domain coverage 143/144 → 144/144 を達成した。
doResendActivationMail(認証メールを再送する) — EC-CUBEadmin_customer_resendルートから導出。管理画面の会員一覧から ADMIN が 仮会員(customerStatus = 1)へメール認証(本登録)メールを再送する。 ALPS type はunsafe(送信のたびに新規メールが発生)。- shape は
doSendShippingNotifyMailを踏襲 — admin-only / unsafe / メール送信という同型の遷移。MailerInterfaceにsendCustomerActivation(string $email, string $secretKey)を追加 (FakeMailerがcustomerActivationsに記録)。 - AUTHZ ラダー(クロスファイアウォール → 存在 → 状態の順):
管理者セッション無し →
UnauthorizedAdminAccessException(403)→ メール未解決 →CustomerNotFoundException(404、既存例外を再利用)→ 対象が仮会員でない(既に本会員) →CustomerAlreadyActivatedException(409、新規ドメイン例外)。既に本会員へ再送するのは無意味な要求なので silent success ではなく明示的な 4xx で返す。 - リソース
Page\Admin\Customer\ResendActivationMail(page://self/admin/customer/resend-activation-mail、POST、CSRF ガード)。 Aura route map にadmin_customer_resendを登録。 Final の公開面はcustomerId/emailのみ —secretKeyはメール本文 専用トークンなので echo しない。 - テストは Fake のみ(モック禁止)。seed
provisional@example.com(customerStatus = 1、secretKey保持)を happy-path の仮会員ターゲット、alice@example.com(customerStatus = 2)を 409 ケースに使用。
- shape は
Phase B — ハイパーメディア / HTTP テストフレーム整備
html コンテキストでカート追加(POST /products/add_cart → 201)後の
GET /cart が空になる不具合(FakeCartStorage がリクエスト毎のインメモリ
Singleton で、別 PHP プロセスのリクエスト間でカートが永続しない)を契機に、
テスト層の不備が判明した。BeMart は BEAR.Skeleton の 3 層テスト構造で
scaffold されておらず、ワークフロー assertion が in-process でしか走らず、
実 HTTP / Cookie 境界を一度も越えていなかった。
- カート修正(
cb4739d)—FakeCartStorageがAPP_CONTEXT=html+ active session 時に fixture を$_SESSION(bemart_html_cartsキー)へ ミラー。Cart/Cart\Itemは session prefix をHtmlCartSession経由で 導出(ハードコード定数を廃止)。汎用RuntimeExceptionはFakeCartFixtureExceptionに置換。 - 3 層テストフレーム(
6b03171)—phpunit.xmlをresource/hypermedia/httpの 3 suite 構成に。tests/Hypermedia/WorkflowTestが storefront 購入動線をRoutedResource経由で in-process 駆動し、tests/Http/WorkflowTestはそれを継承してsetUp()でHttpResourceに差し替えるだけ(同一 assertion を 2 トランスポートで実行)。hypermedia層は 1 プロセス・1 injector で workflow を通すため DI Singleton がテスト全体で生き続けるのに対し、http層は実 HTTP 経由で毎リクエスト injector を再構築し session cookie のみを引き継ぐ — リクエストスコープの Singleton に状態を持つ バグ(インメモリカート等)はhttp層でしか捕捉できない。スタッシュ 証明で「カート修正を外すとhttpのみ赤・hypermediaは緑」を確認した。 詳細はtests/README.md。 - HttpResource を
koriym/php-serverベースへ是正(dev 依存追加)— 当初HttpResourceはリクエスト毎にphp-cgiをproc_openする 383 行の自前実装だった。サーバライフサイクルを BEAR 自身のテスト基盤が 使う保守済みコンポーネントkoriym/php-server(php -S管理)へ委譲し、 curl の cookie jar で session を引き継ぐ薄い実装に置換。スケルトン標準のHttpResourceは stateless JSON API 向けで cookie を扱わないため、 cookie jar のみが BeMart 固有の追加点。aura/installer-default(aura/input経由の旧 Composer プラグイン)はallow-pluginsで 明示的に無効化(false)して install ブロックを解消。 - 暫定事項(将来の整理候補)—
RoutedResourceは BEAR ネイティブの#[Link]/crawlでなくAura.Router の shim、canonicalizeFormFieldsが wire フィールド名(_token/product_id)をリソース引数名へ手で 詰め替えている。
2026-05-23 — EC-CUBE実サイト探索とHTML導線安定化の保存前サマリ
長いセッションで、コード上のRouteMap/Twig棚卸しだけでなく、起動中のEC-CUBE参照サイト http://127.0.0.1:8081 とBeMart http://127.0.0.1:8080 を実HTTPで探索した。詳細は docs/ec-cube-site-exploration-gaps-2026-05-23.md と docs/html-screen-migration-matrix.md。
実施した主な変更
- EC-CUBE由来のstorefront/admin静的アセットを
public/assets/**,public/bundle/**,public/template/admin/assets/**に追加し、CSS未適用状態を解消する基盤を入れた。 public/index.phpとsrc/Http/EccubeRouteMap.phpで、EC-CUBE route名・friendly URL・未実装fallback・HTTPエラー処理を整理した。raw Fatal / Unbound はHTMLに漏らさず、未対応非画面アクションは/__not-implemented?route=...+ shared JS alertへ流す。- HTTPセッションアダプタを追加し、管理ログインと会員ログインをブラウザセッションで扱えるようにした。
- Storefrontは header/search/logo/login/cart/category-nav/footer の共有Block first sliceを追加し、商品一覧はカテゴリ/表示件数/並び順/一覧カート投入フォームまで拡張した。匿名MYページ系はEC-CUBE同様
/loginへ誘導する。 - Product bodyを画像・カテゴリ・タグ・規格名でenrichし、投入商品を
彩のジェラートセットとして画像付き表示にした。 - Adminは
/admin/product/new,/admin/order?orderNo=...,/admin/customer?customerId=..., category list/edit, template add first sliceを接続し、admin nav/submenuをEC-CUBE相当に寄せた。 - ALPSに
page*/route-ec-cube/migration-targettaxonomyとAdminOrderEditPage/AdminCustomerEditPageを追加し、画面状態の追跡粒度を補強した。
実サイト探索で確認した残差
- Product: 商品規格行列、画像アップロード、カテゴリ/タグ実編集、在庫無制限、販売種別、通常価格、販売制限、発送日目安。
- Order: 受注新規、詳細検索、購入者/配送先/明細/支払/対応状況/出荷通知/メール履歴。
- Customer: 管理会員新規、詳細検索、購入履歴、配送先一覧、お気に入り、ステータス操作。
- Content/Setting: ファイル管理、メンテナンス、特商法、定休日、ログイン履歴、ログ表示、システム情報、マスタデータ。
新しいSQL境界ルール
ユーザー指示により、今後の新規SQL Query/CommandではPHP実クラスにPDOクエリを書かず、Ray.MediaQueryを使う。開発順は Fake → EC-CUBEスキーマ照合 → Ray.MediaQuery SQL → Resource/Form → Twig/Browser。既存の Sql*Query / Sql*Command は今回は変更せず、後でまとめて移行する。詳細ルールは docs/skills/G-24-ray-media-query-boundary.md。
検証メモ
asd --validate alps.jsonOK。- Product/Customer/Order/Category/Template周辺のResource/HTML render testsを局所実行してOK。
tests/Http/WorkflowTest.phpはkoriym/php-serverで実HTTPサーバをテスト内起動し、tests/Hypermedia/WorkflowTest.phpと同じ storefront purchase spine を Cookie 境界込みで検証する。起動中の8080に対するHTTP smokeでも主要URLが200/303で応答した。- Codex in-app browser自動操作APIは
No active Codex browser pane availableで取得不可だったため、スクリーンショット付き操作ではなく実HTTP探索で代替した。
2026-05-23 — Session close: HTML migration save/rebase/push handover
This section closes the long HTML-migration session and records the exact repository state to resume from.
Repository state
- Working directory:
~/git/BeMart - Branch:
be-first-migration-bootstrap - Remote:
origin https://github.com/be-framework/BeMart.git - Pushed head:
e651b5e Align template upload screen with upstream port - Push completed:
origin/be-first-migration-bootstrapis in sync with localbe-first-migration-bootstrapat close time.
Commits added after rebasing onto origin
The local save was first split into meaningful commits, then rebased over the upstream work that had landed during the side session. Static assets and the large test commit overlapped with upstream and were dropped/skipped where upstream already carried the better version. The final pushed delta is:
d0a9587 Stabilize HTTP route dispatch and sessions- After rebase, this mostly contributes the shared unsupported-feature JS fallback because upstream already contains the shared Aura.Router front-controller implementation.
bc7825e Improve storefront product and customer flows- Product list/detail enrichment, storefront blocks, images/categories/tags, cart/session fixes preserved where not already upstream.
da5a2fd Add admin product customer order screen slices- Admin product/customer/order/category/template first-slice screens preserved where not already upstream.
b121e62 Document Ray.MediaQuery boundary rule- New skill doc:
docs/skills/G-24-ray-media-query-boundary.md.
- New skill doc:
3f08bf1 Update migration status and handover docs- Exploration gaps, HTML screen matrix, link audit, state notes.
11f0e5a Add malt local runtime configmalt.json+ reusablemalt/conf/*; runtime logs/tmp/DB files remain ignored.
e651b5e Align template upload screen with upstream port- Post-rebase cleanup: kept upstream’s EC-CUBE-faithful
Store/template_add.twigport so tests and template cache agree.
- Post-rebase cleanup: kept upstream’s EC-CUBE-faithful
Rebase notes
- Before push, local was
ahead 9, behind 27againstorigin/be-first-migration-bootstrap. - Rebased instead of force-pushing. No force push was used.
- Upstream already contained EC-CUBE static assets, so the local static-asset commit was dropped as patch-equivalent.
- The previous local
tests/Http/HttpHypermediaTest.phpcommit conflicted with upstream’s newer 3-tier test framework (tests/Hypermedia/WorkflowTest.php+tests/Http/WorkflowTest.php), so it was skipped in favor of upstream. composer installwas required locally after rebase becausecomposer.lockalready includedkoriym/php-server, but the localvendor/did not yet have it.
Verified before close
asd --validate alps.json
rm -rf var/tmp/html
./vendor/bin/phpunit \
tests/Resource/ProductsHtmlRenderTest.php \
tests/Resource/ProductHtmlRenderTest.php \
tests/Resource/AdminProductResourceTest.php \
tests/Resource/AdminTemplateAddHtmlRenderTest.php \
tests/Http/WorkflowTest.php \
--stop-on-failure
Result: ALPS valid; selected PHPUnit green — 23 tests / 114 assertions, with expected deprecations/skips.
Non-negotiable next-session rules
- New SQL Query/Command boundaries use Ray.MediaQuery (
#[DbQuery]interface + SQL file). Do not add new concrete PHP PDO query classes. - Existing
Sql*Query/Sql*Commandimplementations stay as-is until a dedicated batch migration. - Development order remains Fake → EC-CUBE schema check → Ray.MediaQuery SQL → Resource/Form → Twig/Browser.
- JS alert is only a safety net for non-screen/unsupported actions. The main job remains EC-CUBE routable HTML screen migration and browser-flow parity.
- Unsupported links/buttons must stay visible; they should fail safely via alert and
/__not-implemented?route=..., not via hidden links, raw Fatal, or Unbound.
Recommended first prompt for the next session
~/git/BeMart で作業します。branch は be-first-migration-bootstrap です。
前セッションは e651b5e まで push 済みです。
まず docs/HANDOVER.md, docs/HOW_TO_CONTINUE.md, docs/migration-status.md,
docs/html-screen-migration-matrix.md, docs/skills/G-24-ray-media-query-boundary.md を読んで、
残りHTML画面移植を進めてください。
新規SQL境界は Ray.MediaQuery を使い、既存PDO実装は今すぐ移行しないでください。
2026-05-26 — HTML route coverage and SQL baseline handover
完了事項
- Aura route extras の
unsupported-route到達を 0 に整理。 - HTML 入口の公開 method を GET / POST のみに統一。
- HTML POST から内部 Resource の PUT / DELETE へ dispatch する設計を固定。
- 管理画面 route alias と query/form param map を補完。
BadRequestExceptionを HTTP response に変換し、欠落 parameter などで raw Fatal が出ないようにした。- prod context で Be Final が解決できるよう、production-safe default service bindings を追加。
- SQL suite は MariaDB baseline と明記し、非MariaDB環境では skip するようにした。
追加ドキュメント
~/git/BeMart/docs/methodology/html-route-coverage.md~/git/BeMart/docs/methodology/sql-test-baseline.md
検証結果
vendor/bin/phpunit --filter 'RouterTest|TemplateRouteCoverageTest|CsrfProtectionCoverageTest' --colors=neverOKvendor/bin/phpunit tests/Resource --filter HtmlRender --colors=neverOKcomposer test:fake -- --colors=neverOKcomposer test:sql -- --colors=neverOK扱い(非MariaDBのため 742 skipped)composer test:http -- --colors=neverOKcomposer psalm -- --output-format=consoleNo errors found- ローカルリンククロール: 158 pages / 158 links / problems 0
次回注意
- Twig に route name を追加したら Aura route map と coverage test を同時に更新する。
- HTML には PUT / DELETE を出さない。更新・削除は POST form で送る。
- SQL suite の green 判定は MariaDB で行う。MySQL 8/9 での大量失敗は baseline 違いとして扱う。
2026-09-14 — event-sourcing 観測ログ: credential redaction と replayable の契約
bear/event-sourcing(ObserveModule)が記録する観測ログ・抽出イベントストリームから
credential を守る作業。request params はライブラリの SensitiveParamsFilter(BeMart は
resetKey/authKey を追加 substring として渡す)、page:// レスポンス body は
ExcludedResponseBodyStore、PHP 例外スタックトレースは #[SensitiveParameter]
(credential 名パターンに一致する Resource on* 引数と Be Input/Final/Being の string
コンストラクタ引数すべて)で守る。
現在の redaction 範囲と replayable の契約は
event-sourcing-observation.md を正とする — ここでは
構築の経緯のみ記録する。
経緯: ライブラリ側の契約変更に追従した
作業途中で bearsunday/BEAR.EventSourcing#22
がマージされ(1.x @ 8616c825)、dev-redact-params @ ac169f37 から 18 commit 進んだ。
うち2点が BeMart 側の契約を反転させた:
- filter は key を 削除しない — key は残り、値が
SensitiveParamsFilter::FILTEREDになる。 ライブラリのSensitiveParamsFilterがコンストラクタで追加 substring を受け取るように なったので、BeMart 独自のAppParamsFilter(top-levelunsetのみ、ネスト非対応)は weightless になり 削除した。ObserveModuleはtoInstance(new SensitiveParamsFilter(['resetKey', 'authKey']))を束縛する。 replayable:falseの request は 抽出される(Event::$replayable === false)。 「成功した credential 付き書き込みがイベントストリームから丸ごと消える」という以前の 帰結は消滅した。
composer.json は dev-redact-params → 1.x-dev に張り替え(composer はブランチ名が
数字始まりだと dev-1.x ではなく 1.x-dev と表記する)。タグ付きリリースはまだ無い。
.claude/skills/bear-observe/ のコピーは vendor と byte 一致するよう再同期(BeMart 側で
編集していなかったので上書きのみ)。bear/resource の floor 1.31 は 1.34.0 で充足。
sql/event_store のコピーは BeMart に無いので schema 移行は不要。
完了事項
src/Module/ExcludedResponseBodyStore.php(新規)、src/Module/AppParamsFilter.php(新規→ ライブラリ追従で削除)、#[SensitiveParameter]の Resource/Input/Final/Being 層への付与、TwoFactorAuthConfigured::$authKeypublic property の削除、discovery ベースの境界契約テスト 2本(tests/Resource/CredentialParameterSensitiveParameterTest.php、be/tests/Domain/CredentialConstructorParameterSensitiveParameterTest.php— 手動監査はEntry::onPost・Reset::onGet・Be 層 7 引数を見落とした)。- 検証:
EventSourcingExtractionTest/ExcludedResponseBodyStoreTest/両 discovery テスト green、 フルスイート green、psalm 0 error。プロジェクト全体の phpcs はgetenv()禁止の pre-existing 違反 (4ファイル5件、bin/page.php・public/page.php・src/Compatibility/Eccube/EccubeCustomizeAssetWriter.php・src/Compatibility/Eccube/EccubeTemplateCompatibility.php、このブランチの変更とは無関係) が残るため clean ではない。