BeMart

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/                            ← 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 切り出し

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 関係:

Quality refinement (Opus 4.7 session)

誤解を招く doc の改善、似た名前ペアの曖昧さ解消、仕様書 (doc4) 由来の拡充、ERD 由来の新規 descriptor 追加、123 件の S001 suggestion 解消。

verify-* 由来:

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

Pilot 1 で見つかった workflow prompt の穴


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):

  1. 初版 → Phase 8 reflection で「Cascade Diamond」と分類
  2. self review 1 で「1 つの Final 内に 5 ブロックを手続き的に並べただけで、Being 連鎖も #[Reason] 収束も無い」と判明し Linear / Minimal へ再分類 (commit 8f75c66)
  3. self review 2: 真の Cascade として refactor — QuantityAdjusted Being を導入し、Stage 1 (quantity 確定 + cartKey 解決) → Stage 2 (cart 文脈 + マージ + 配送 + 保存) の 2 段 cascade に。Final で 3 つの独立 Reason (CartQuery + CartCommand + ProductClassQuery) が #[Inject] で収束 (commit 35a0201)
  4. self review 3 (2026-05-18): 「Final が永続化以外に in-memory merge + totalPrice + deliveryFeeTotal 計算を抱えていて厚い」という違和感から、CartMerged Being を抽出。3 段 cascade に再構成 — Stage 1 (QuantityAdjusted: ProductClass lookup / Stock cap / SaleLimit cap / cartKey) → Stage 2 (CartMerged: 既存 cart 取得 / item merge / totalPrice / deliveryFeeTotal) → Stage 3 (CartItemAdded Final: 永続化のみ)。Final の #[Inject] は CartCommand 1 つに収束。Linear 版の snapshot は be/docs/variations/linear-doAddCartItem/ に保存 (commit 8f75c66 時点のコード)。本評価は be/docs/be-adoption-evaluation.md に詳述。Cascade Diamond reference (be-patterns order-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:

.claude/prompts/application-implement.md:


Orphaned states

解消済み: ShoppingError (goShoppingError transition を追加)

残存 (意図的なデータ合成パターン):

これらは「真の孤立状態」ではなく、親 list state から href で参照されるデータ合成要素。


次の AI / 人間レビュアーへの助言

ALPS 品質

Workflow

Pilot

振り返り方


Pilot 3 — doConfirmOrder (Branching + Cascade Diamond 不成立の発見)

目的: Branching パターン (Final が分岐) と Cascade Diamond (apex Moment 並列収束) の 2 つを同時に検証する設計だったが、Diamond 側は be-framework のメカニクス上不成立と判明し、Linear Cascade + Branching に縮退した。

改訂履歴 (2026-05-18):

  1. 初版: ConfirmOrderInput → OrderConfirming (Cascade Diamond apex; #[Inject] で PreOrderResolved + PurchaseFlowApplied + PaymentVerified を並列収束) → OrderConfirmed | OrderConfirmFailed を想定
  2. 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 なしと判定して落ちる
  3. 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] のみ理解する
  4. 構造的結論: 「#[Input] を必要とする Being」は #[Inject] の対象になれない。Cascade Diamond の apex (#[Inject] で複数 Moment を並列収束) は、各 Moment が Input 依存ゼロの場合のみ成立する。EC-CUBE の doConfirmOrder のように Input scalar から派生して Service 呼び出しを並列する用途は不成立 → 4 段 Linear Cascade に再構成 (各 Being が #[Input] public プロパティで下流に scalar を forward)
  5. 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 で新規追加した Reason / Semantic 群:

Pilot 3 の参照ドキュメント

Pilot 1+2+3 を通じた集計


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):

  1. ALPS 分析 → doRegisterCustomer (4 必須フィールド) + CustomerRegistration container (11 オプショナル) を Input にマップ。CustomerRegistrationComplete.descriptor = [#goTop] のみ → #[Link(rel: 'goTop', ...)] 1 件
  2. パターン判定: 4 つの Reason が互いに独立で並列実行可能 → Multi-Reason Being (blog-publishing 準拠)。Diamond ではない (各 Reason の結果が他の Reason に流れない)
  3. domain-review subagent → pass (findings 7 / blocking 0)。指摘で実装に反映: (a) InitialPoint / CustomerStatus の docblock 主張をバリデータと整合させる, (b) FakeCustomerStorage::getByEmail() を追加してテストの Reflection を除去
  4. application-review subagent → pass (findings 0 / blocking 0)
  5. 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 で新規追加した Be 層 (be/src/):

Pilot 4 で新規追加した BEAR 層:

Pilot 4 の security 観点 (security-review findings から記録)

Pilot 4 の参照ドキュメント

Pilot 1+2+3+4 を通じた集計

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):

  1. ALPS 分析 → doCheckout (paymentMethod client-input) + preOrder 既存 → CheckoutInput(preOrderId, paymentMethodId) にマップ。ShoppingComplete.descriptor から #[Link(rel: 'goTop')] + #[Link(rel: 'goCart')] を導出
  2. パターン判定: 3 Reason が strict sequence (inventory before payment before number-gen) → Diamond-Cascade (loan-application 準拠)。Pilot 4 の Multi-Reason Being (並列 Reason) と区別: Pilot 5 は順序依存 + Final で複数副作用
  3. 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 に固定 → 解決
  4. domain-review subagent → pass (findings 5 / blocking 0)。指摘は全て non-blocking style nit (Final public surface が ALPS ShoppingComplete より広い / DateTimeImmutable 直生成 / FinalizedOrderEntity inline 生成 / FakeInventoryAllocator atomicity コメント / AppModule コメント評価)
  5. 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 / @var PHPDoc 追加)
  6. 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 で新規追加した Be 層 (be/src/):

Pilot 5 で新規追加した BEAR 層:

Pilot 5 の security 観点 (security-review findings から記録)

Phase B / Phase 2 に着手すべき項目 (security-review 13 finding の要約):

Pilot 5 の参照ドキュメント

Pilot 1+2+3+4+5 を通じた集計


Phase B — Security / Hardening Slice 1: Psalm Setup

目的: 静的解析を Phase A コードに後付けで導入し、Phase B 以降の判定基盤を整える。決定的要素側に置ける security チェック を 1 つ確立する。

完了した作業

Slice 1 の現状認識 (重要)

composer psalm-taint は exit 0 だが、これは「Phase A コードベースで Psalm の標準 taint mechanism が検出する flow が存在しない」ことを意味する。「PII flow が無い」ことではない。

Phase A の構造的事実:

→ 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 に対応させる)

未実施。実施候補:

判断: Slice 1 では Phase B の 足場を作る ことに集中。アノテーション戦略は Phase B Slice 2 で別途決定。

Slice 2 以降の Phase B 項目 (security-review の累積 finding から)

未着手:

Slice 1 の振り返り (決定的/非決定的)


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 する形を取る。

完了した作業

確認結果

Slice 3 の振り返り (決定的/非決定的)

今後の Phase B Slice (推奨順)

候補 性質 一行コメント
Slice 4: Mass-assignment fix (Pilot 5 F-2) 決定的 ✅ 完了 (下記 Slice 4 セクション参照)
Slice 5: env-gated entry point 決定的 ✅ 完了 (下記 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 に定めていた:

つまり以前から 「テストで指定していた paymentMethodId は OrderEntity と一致していた」。Slice 4 は invariant を「契約として明文化」しただけ。テストの assertion 値 (total 2250, captures[paymentMethodId]=2 等) は一切変更不要。

動作確認

Slice 4 の振り返り (決定的/非決定的)

残課題 (Slice 4 由来)


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   # ← 書かれていない

設計上の判断と注釈

Slice 5 の振り返り (決定的/非決定的)

次の 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 つあった:

  1. $_SESSION global を直接読む procedural helper — Be / BEAR どちらの DI 哲学にも合わない。テストで session を切替えるのも難しい。却下
  2. SessionInterface を #[Inject] する Reason — Be Reason として扱う。Fake は memory、本番は $_SESSION 読みアダプタを後付け差し替え。採用
  3. 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 つの理由:

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 では:

これは設計上の選択であり、上記の timing attack 注釈の通り完璧な存在隠蔽ではない。Slice 6 では「明確な区別」 を取り、Slice 7+ で「存在隠蔽の強化」 を別途検討する。

Test 戦略: rebindSession() helper

PHPUnit の setUp() で default customer-001 を bind、各テストで必要なら rebindSession(...) で injector を作り直す。理由:

新規 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 の振り返り (決定的/非決定的)

次の 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 (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');
}

この 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 だけを使う。

これにより:

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 (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 する方が手戻りが少ない)。

追加 / 変更ファイル

新規:

変更:

EC-CUBE 側 contract (Slice 8 では実装しない)

EccubeSharedCsrfTokenAdapter の本番動作には EC-CUBE 側で以下の協力が必要:

  1. 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);
    }
    
  2. 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 にはならない。

検証

設計上の判断

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 ではない。

そのため 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 の振り返り (決定的/非決定的)

次の 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 必要)」を解消できる。

追加 / 変更ファイル

変更のみ (新規なし):

Honest finding (重要) — Be Framework は Psalm から opaque

annotation 追加後も composer psalm-taint は 0 errors のまま。これは「flow が存在しない」 のではなく「Psalm の data-flow analyzer が Be Framework の #[Be] cascade を辿れない」ためである。

具体的な観察:

  1. Shopping/Checkout::onPost($preOrderId) に @psalm-taint-source input $preOrderId を付与
  2. その値が new CheckoutInput(preOrderId: $preOrderId) に渡る — ここまでは Psalm が見える
  3. ($this->becoming)($input) で metamorphose — BecomingInterface::__invoke() は object を返すので、Psalm は次の class が CheckoutPrepared であることを知らない
  4. CheckoutPrepared::__construct(#[Input] string $preOrderId, ...) で再度 string を受けるが、Psalm にとってはこの string は別の origin から来た新規変数
  5. 以降 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 を踏襲する:

検証

設計上の判断

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 の振り返り (決定的/非決定的)

次の 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

Pilot 7 — doActivateCustomer

Pilot 8 — doUpdateCustomer

Batch 1 振り返り (決定的 / 非決定的)

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

Pilot 10 — doUpdateCartItemQuantity

設計上の発見 (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 へ流れる。

選択肢:

これは G-15 (Multi-side-effect Final 判定基準) と同列の重要発見として SKILL.md 候補に記載すべき。Be Framework での「同じ前処理を異なる下流に向ける」 ケースの規約として Input-per-intent + Being-per-shape を踏襲する。

Pilot 11 — doRemoveCartItem

Batch 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

Pilot 15 — doSubmitContact

Pilot 14 — doRequestPasswordReset

Pilot 12 — doReorder (本 Batch では deferred)

ALPS doc: 「過去の受注内容をカートに再投入する。在庫切れ商品はスキップ、現在価格を適用」

Deferred の理由:

  1. FinalizedOrderEntity に items が含まれていない (Pilot 5 で order item は別 table と decided)
  2. OrderQueryInterface に itemsByOrderNo が無い
  3. Fake fixture var/fake/orders.json に items column が無い
  4. これらすべての追加は 2-unit 規模 (Pilot 1 件分 + infrastructure)

次の Batch (Pilot 12 含む) で着手するべき先行作業:

Batch 3 振り返り (決定的 / 非決定的)

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:

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)

Pilot doLogout (Agent E)

goMypage (Agent F)

doWithdrawCustomer (Agent G)

Cherry-pick 衝突対応

Agent D (Pilot 12) と Agent F (goMypage) は両者 OrderQueryInterface / FakeOrderQuery を拡張 (D: byOrderNo、F: listByCustomer)。git auto-merge が成功 — 異なる method を追加していたため。Wave 設計時の disjoint-files 原則が機能した。

Orchestration の振り返り

累計進捗 (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:

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)。

発見

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 は欠落。

採用した対応:

G-18 として記録: ALPS と実装の往復は片方向ではない。実装 agent が「ALPS にあるべきだがない」 transition を見つけた場合、agent が ALPS を勝手に編集するのではなく、(a) conventional 名で実装 (b) gap を docblock + return message で報告 (c) orchestrator が ALPS の整合性を取る、の責務分担を採用。

admin AAA infrastructure (新設)

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 件。

発見

累計進捗 (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:

新 skill gap

alps.json update

orchestrator が事後追記:

これで 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 で発見:

G-21: idempotent DELETE の 2 つのスタイル

Wave 6 で 2 つの DELETE 実装が並存:

規約: 一般的な “rare-but-OK” path (お気に入りを 2 回押した) は silent、本当に変更を期待する path (cart や住所) は 404。Wave 6P がこの規約を明文化。

G-22: pagination の Semantic 命名

Wave 6R goOrderHistory で、既存 Limit Semantic (1-50) を再利用するか別 HistoryLimit Semantic を作るかが論点に:

累計進捗 (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) の運用パターン:

  1. orchestrator が wave 設計時に dependency 表を作成 (Wave 4 で漏れて Wave 5 brief で補完)
  2. agent 並列度は 3-4 が安全圏 (Wave 2 で 4 agent、Wave 5/6 で 3-4 agent)
  3. 各 agent には briefing で「STOP and report」 ルートを明示 → S1-S7 stop condition 相当の自律判断を委譲
  4. 完了通知ごとに orchestrator が cherry-pick + integrated test/psalm verify → push
  5. 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) で確立した規約

W (guest checkout) のスコープ判断

X (SKILL bake) の構造

累計進捗 (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 (累計):

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 衝突

G-24: 同名 Semantic class の意図せぬ重複作成

Stale autoload cache の罠

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 で確定した最終事項

累計進捗 (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 (累計):

構築のフェーズ移行

Phase A (transition 量産) はこのセッションで完了。次の Phase 2 の主作業:

  1. Fake → real persistence: 全 Fake*Storage を Doctrine / Ray.MediaQuery 等に置換
  2. Fake → real services: FakeMailer → EC-CUBE MailService, FakePaymentGateway → 本物 PSP, FakePurchaseFlow → EC-CUBE Service
  3. Slice 7.2 / 8.2: EC-CUBE 側 EventListener で session / CSRF mirror
  4. stub 解消: 上記 Wave 8-9 で stub になった 7 件の本物実装
  5. 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 が正。本セクションは構築プロセスの記録。

スコープと成果

主要な決定

現行スキーマファイルについての注記(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 を指すよう更新済み。

積み残し

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 が正。本セクションは構築プロセスの記録。

スコープと成果

主要な決定

積み残し

Admin HTML — section-wave 並列移植(Tier-1 完了)

ストアフロント完了後、admin テーマ(EC-CUBE template/admin/)の移植に着手した。

Tier-1 / Tier-2 の切り分け — 各 section-wave は「BEAR リソースが既に GET を提供する ページ(list/data + 単純 CRUD)」= Tier-1 を移植し、以下を Tier-2 として defer した:

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 参照)。

回収済み計 29 ページ → admin HTML は 77 ページ中 63 ページ移植。残 14 ページは Store/Plugin の install/search サブツリーのみ — プラグインは移植対象外のため、 スコープ内の admin HTML 移植は完了。

Phase 3 storefront — 仕上げ

Phase B — セキュリティ・本番化

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 を達成した。

Phase B — ハイパーメディア / HTTP テストフレーム整備

html コンテキストでカート追加(POST /products/add_cart → 201)後の GET /cart が空になる不具合(FakeCartStorage がリクエスト毎のインメモリ Singleton で、別 PHP プロセスのリクエスト間でカートが永続しない)を契機に、 テスト層の不備が判明した。BeMart は BEAR.Skeleton の 3 層テスト構造で scaffold されておらず、ワークフロー assertion が in-process でしか走らず、 実 HTTP / Cookie 境界を一度も越えていなかった。

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。

実施した主な変更

実サイト探索で確認した残差

新しい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。

検証メモ

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

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:

Rebase notes

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

~/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

完了事項

追加ドキュメント

検証結果

次回注意

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 側の契約を反転させた:

  1. filter は key を 削除しない — key は残り、値が SensitiveParamsFilter::FILTERED になる。 ライブラリの SensitiveParamsFilter がコンストラクタで追加 substring を受け取るように なったので、BeMart 独自の AppParamsFilter(top-level unset のみ、ネスト非対応)は weightless になり 削除した。ObserveModule は toInstance(new SensitiveParamsFilter(['resetKey', 'authKey'])) を束縛する。
  2. 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 移行は不要。

完了事項