docs(18.1-06): validated phase 18.1 verification map and deferred items (task 4)

- 18.1-VALIDATION.md: every task of plans 01 to 06 with its command and the
  gate result; status validated, nyquist_compliant and wave_0_complete true
- deferred-items.md: reorder.flat and the pnpm store watch resolved; the
  layer's check-git-install.sh stays open (no gate or release step runs it)
This commit is contained in:
Jakub Zych
2026-10-14 21:05:47 +02:00
parent 7790434834
commit 5c2b0e1d8a
2 changed files with 68 additions and 38 deletions

View File

@@ -3,10 +3,11 @@ phase: "18.1"
slug: "payment-gateway-subscriptions"
# status lifecycle: draft (seeded by plan-phase) → validated (set by validate-phase §6)
# audit-milestone §5.5 distinguishes NOT-VALIDATED (draft) from PARTIAL (validated + nyquist_compliant: false) (#2117)
status: draft
nyquist_compliant: false
wave_0_complete: false
status: validated
nyquist_compliant: true
wave_0_complete: true
created: "2026-10-14"
validated: "2026-10-14"
---
# Phase 18.1 — Validation Strategy
@@ -19,12 +20,12 @@ created: "2026-10-14"
| Property | Value |
|----------|-------|
| **Framework** | Go `testing` + testcontainers Postgres 16 (Docker tests skip under `-short`); layer: `node --test` via `pnpm test` |
| **Config file** | per-repo `testmain_test.go` (copy of P18 `sm-pgstripe-plugin/testmain_test.go`: `flag.Parse()` + `testing.Short()`) — Wave 0 for the new pgsub repo |
| **Framework** | Go `testing` + testcontainers Postgres 16 (Docker tests skip under `-short`); admin: vitest; layer: `node --test` via `pnpm test` |
| **Config file** | per-repo `testmain_test.go` (`flag.Parse()` + `testing.Short()`): framework `modules/cabana`, `sm-paymentgatewaysubscriptions-plugin` (root), `sm-pgstripe-plugin` (root and `subscriptions`) |
| **Quick run command** | `go -C ../sm-paymentgatewaysubscriptions-plugin vet ./... && go -C ../sm-paymentgatewaysubscriptions-plugin test -short ./...` ; `go -C ../sm-pgstripe-plugin vet ./... && go -C ../sm-pgstripe-plugin test -short ./...` |
| **Full suite command** | same with `go test ./...` (Docker) in both plugin repos; `pnpm -C ../vue-paymentgateway-layer test`; `go vet ./... && go test -short ./...` in `summercms.go` |
| **Phase gate** | `scripts/check-phase18.1.sh` in `summercms.go` (precedent `scripts/check-phase18.sh`) |
| **Estimated runtime** | quick ~30 s per repo; full ~3–5 min (containers) |
| **Full suite command** | `go test ./...` (Docker) in both plugin repos; `pnpm -C ../vue-paymentgateway-layer test`; `go vet ./... && go test -short ./...` in `summercms.go` |
| **Phase gate** | `scripts/check-phase18.1.sh --all` in `summercms.go` (stages go, plugins, admin, layers, docs, postgres; `--self-test`; `PHASE181_REQUIRE_POSTGRES=1` makes a missing Docker daemon fatal) |
| **Estimated runtime** | quick ~30 s per repo; full gate ~10 min (containers, vitest, docs) |
---
@@ -32,29 +33,54 @@ created: "2026-10-14"
- **After every task commit:** the `-short` quick run of the touched repo
- **After every plan wave:** full suite of the touched repos (Docker)
- **Before `/msd-verify-work`:** `scripts/check-phase18.1.sh` green
- **Before `/msd-verify-work`:** `scripts/check-phase18.1.sh --all` green
- **Max feedback latency:** 60 seconds for the quick run
---
## Per-Task Verification Map
Filled in by the planner per task; the success-criterion map below is the contract.
Every task of plans 01 to 06, with the command that proves it (run from the summercms.go root; sibling repositories through `go -C ../<repo>` or `pnpm --dir ../<repo>`). Docker rows ran on a Docker host. "File exists" names the plan and task that created the test. Status is the result of the 18.1-06 gate run (`PHASE181_REQUIRE_POSTGRES=1 bash scripts/check-phase18.1.sh --all`) unless noted.
| Req | Behaviour | Type | Automated command | File exists | Status |
|-----|-----------|------|-------------------|-------------|--------|
| SC-1 | Baseline up/down, idempotent re-run, names match the Horoskopia dump; `users.stripe_customer_id` added only if missing | Docker | `go test ./updates/ -run TestBaseline` (pgsub) | ❌ W0 | ⬜ pending |
| SC-1 | Registry: duplicate alias fails boot; unknown alias errors; `user` subscribable reads/writes `users.stripe_customer_id` | unit + Docker | `go test -short ./... -run TestRegistry`; `go test ./... -run TestUserSubscribable` | ❌ W0 | ⬜ pending |
| SC-1 | Status / grace / feature / days-until-renewal matrices | unit | `go test -short ./... -run 'TestAccess\|TestGrace\|TestFeatures'` | ❌ W0 | ⬜ pending |
| SC-2 | Webhook: 500 no secret, 400 bad sig/payload, 200 duplicate (no job), 200 unhandled type (no job), one job per handled type, both URLs | Docker + `-short` decode | `go test ./subscriptions/ -run TestWebhook` (pgstripe) | ❌ W0 | ⬜ pending |
| SC-2 | Job: marker only on success (STRIPE-006), skip paths keep marker (STRIPE-007), duplicate/concurrent fire once, basil + pre-basil shapes | Docker | `go test ./subscriptions/ -run TestJob` | ❌ W0 | ⬜ pending |
| SC-2 | Event matrix activated / past_due / canceled / reactivated per previous→current; deleted always canceled | unit on `Service.Apply` | `go test ./... -run TestApplyEvents` (pgsub) | ❌ W0 | ⬜ pending |
| SC-2 | `SyncPlanToGateway`: free skip, product create/update, price unchanged / archive+create, stored IDs | fake Stripe | `go test ./subscriptions/ -run TestSyncPlan` | ❌ W0 | ⬜ pending |
| SC-3 | Admin YAML boots, bulk/toolbar sync results, preview partial | Docker smoke | `go test ./ -run TestAdmin` (pgsub) | ❌ W0 | ⬜ pending |
| SC-3 | Customer API authz (401 without JWT, plans public), 404/409/422 paths, redirect URLs from config only | Docker | `go test ./api/ -run TestCustomerAPI` | ❌ W0 | ⬜ pending |
| SC-3 | Layer client and components | node | `pnpm -C ../vue-paymentgateway-layer test` | ❌ W0 | ⬜ pending |
| SC-4 | Host flow: checkout → created → activated; invoice.paid; payment_failed + past_due with grace by clock; cancel at period end → deleted → canceled; reactivation; duplicate idempotency; plan sync; one real conga worker run | Docker | `go test ./subscriptions/ -run TestHostSubscriptionFlow` | ❌ W0 | ⬜ pending |
| SC-5 | PHP test map covered (ReliabilityTest STRIPE-006, DataHandlingTest PAY-10 / STRIPE-001 / STRIPE-003, AuthenticationTest) | gate | `scripts/check-phase18.1.sh` | ❌ W0 | ⬜ pending |
| Task | Req | Behaviour | Type | Automated command | File exists | Status |
|------|-----|-----------|------|-------------------|-------------|--------|
| 18.1-01 T1 | D-17 | taglist array mode: compile gates, save to a JSON column, record GET, OpenAPI, dist, docs | unit + Docker | `go test -count=1 -short -run '^TestTaglistFieldCompile$' ./modules/cabana`; Docker: `go test -count=1 -run '^TestTaglistFieldSmoke$' ./modules/cabana` | ✅ (01 T1) | ✅ green |
| 18.1-01 T2 | D-17 | taglist string mode, customTags, relation and settings save sites, repeater refusal, preview | unit + Docker + vitest | `go test -count=1 -short -run '^(TestTaglistFieldCompile\|TestTaglistFieldValidate\|TestTaglistFieldHydrate)$' ./modules/cabana && npm --prefix admin test -- tests/form/TaglistField.test.ts` | ✅ (01 T2, 06 T2) | ✅ green |
| 18.1-01 T3 | D-17 | flat reorder inference, flat save, `flat` on the list schema (localized too) and the reorder data | unit + Docker | `go test -count=1 -short -run '^(TestReorderFlatCompile\|TestReorderFlatNodes)$' ./modules/cabana`; Docker: `go test -count=1 -run '^(TestReorderFlatSmoke\|TestReorderPostgres)$' ./modules/cabana` | ✅ (01 T3, 06 T2) | ✅ green |
| 18.1-01 T4 | D-17 | flat reorder in the SPA, dist rebuilt | vitest + gate | `npm --prefix admin test -- tests/views/ReorderView.test.ts && bash scripts/check-admin-dist.sh` | ✅ (01 T4, 06 T2) | ✅ green |
| 18.1-01 T5 | D-22 | framework release decision (reply "hold") | checkpoint | n/a (decision; the final push is 18.1-06 T5) | n/a | ✅ recorded |
| 18.1-02 T1 | D-04, D-16 | baseline shape confirmed (reply "baseline-jsonb") | checkpoint | n/a | n/a | ✅ recorded |
| 18.1-02 T2 | SC-1, SC-2 | plugin boots, baseline migrates, Apply stores an active subscription and fires `activated` once | Docker | `go -C ../sm-paymentgatewaysubscriptions-plugin test -count=1 -run '^TestSubscriptionsTracer$' .` | ✅ (02 T2) | ✅ green |
| 18.1-02 T3 | SC-1, D-08, D-09 | registries, Gateway contract, built-in user subscriber, checkout stores the customer id | unit + Docker | `go -C ../sm-paymentgatewaysubscriptions-plugin test -count=1 -short -run '^TestRegistry$' ./contracts`; Docker: `go -C ../sm-paymentgatewaysubscriptions-plugin test -count=1 -run '^(TestSubscriptionsCheckout\|TestUserSubscribable)$' .` | ✅ (02 T3, 06 T3) | ✅ green |
| 18.1-02 T4 | SC-1, SC-2, SC-3, D-03, D-05, D-13 | model checks, features, event diff, invoices, claim, service API, baseline names | unit + Docker | `go -C ../sm-paymentgatewaysubscriptions-plugin test -count=1 -short -run '^(TestSubscriptionChecks\|TestPlanPrices\|TestFeatureList)$' ./models`; Docker: `go -C ../sm-paymentgatewaysubscriptions-plugin test -count=1 -run '^(TestSubscriptionsCoreSmoke\|TestPlanJSONBColumns\|TestApplyEvents\|TestClaimEventConcurrent\|TestBaselineSchema\|TestServiceAPI)$' .` | ✅ (02 T4, 06 T3) | ✅ green |
| 18.1-02 T5 | D-22 | pgsub remote created and main pushed at `aad4647` (reply "create-and-push") | checkpoint | `git -C ../sm-paymentgatewaysubscriptions-plugin ls-remote origin main` | n/a | ✅ recorded |
| 18.1-03 T1 | D-10, D-11, D-18 | webhook contract confirmed (reply "php-plain-text") | checkpoint | n/a | n/a | ✅ recorded |
| 18.1-03 T2 | SC-2, SC-4 | checkout through the Stripe fake, signed `customer.subscription.created`, claim-first job, `activated` | Docker | `go -C ../sm-pgstripe-plugin test -count=1 -run '^TestHostSubscriptionFlow$' ./subscriptions` | ✅ (03 T2) | ✅ green |
| 18.1-03 T3 | SC-2, SC-4, D-11, D-12, D-15, D-19 | gateway, plan sync, five event types in both shapes, app tag, refusals, real worker | unit + Docker | `go -C ../sm-pgstripe-plugin test -count=1 -short -run '^(TestDecodeSubscription\|TestDecodeInvoice\|TestJobArgsMinimal)$' ./subscriptions`; Docker: `go -C ../sm-pgstripe-plugin test -count=1 -run '^(TestHostSubscriptionFlow\|TestSubscriptionWebhookSmoke\|TestSubscriptionJob\|TestSyncPlan\|TestSubscriptionGateway)$' ./subscriptions` | ✅ (03 T3, 06 T4) | ✅ green |
| 18.1-03 T4 | D-18 | subscription secret fields, import target flag, READMEs, settings and import smokes | unit + Docker | `go -C ../sm-pgstripe-plugin test -count=1 -short -run '^(TestStripeDataHandling\|TestSDKPin)$' .`; Docker: `go -C ../sm-pgstripe-plugin test -count=1 -run '^(TestStripeSettingsSmoke\|TestStripeImportWinterSmoke\|TestStripeSubscriptionSecrets\|TestHostStripeFlow)$' .` | ✅ (03 T4, 06 T4) | ✅ green |
| 18.1-04 T1 | SC-3, D-17 | plans admin with permissions, navigation and the taglist form | Docker | `go -C ../sm-paymentgatewaysubscriptions-plugin test -count=1 -run '^TestAdminPlansSmoke$' .` | ✅ (04 T1) | ✅ green |
| 18.1-04 T2 | SC-3, D-13, D-17 | flat plan reorder, bulk and toolbar sync with per-plan failures | Docker | `go -C ../sm-paymentgatewaysubscriptions-plugin test -count=1 -run '^(TestAdminPlansSmoke\|TestAdminSyncActions)$' .` | ✅ (04 T2, 06 T3) | ✅ green |
| 18.1-04 T3 | SC-3, D-02, D-08 | subscriptions admin, label field types, EN/PL phrasebook | unit + Docker | `go -C ../sm-paymentgatewaysubscriptions-plugin test -count=1 -short -run '^(TestAdminCatalog\|TestAdminLangParity\|TestLangParity)$' .`; Docker: `go -C ../sm-paymentgatewaysubscriptions-plugin test -count=1 -run '^TestAdminSubscriptionsSmoke$' .` | ✅ (04 T3, 06 T3) | ✅ green |
| 18.1-04 T4 | D-13 | customer API contract confirmed (reply "api-research") | checkpoint | n/a | n/a | ✅ recorded |
| 18.1-04 T5 | SC-3, D-13, D-21 | customer API endpoints, redirects from config, authorization matrix | unit + Docker | `go -C ../sm-paymentgatewaysubscriptions-plugin test -count=1 -short -run '^TestRedirectURLs$' .`; Docker: `go -C ../sm-paymentgatewaysubscriptions-plugin test -count=1 -run '^(TestCustomerAPISmoke\|TestCustomerAPIAuthz)$' .` | ✅ (04 T5, 06 T3) | ✅ green |
| 18.1-05 T1 | SC-3, D-14 | typed client, PgPlans, PgSubscribeButton with a one-time checkout redirect | node | `pnpm --dir ../vue-paymentgateway-layer test` (files `tests/subscriptions.test.ts`, `tests/subscribe.test.ts`) | ✅ (05 T1, 06 T2) | ✅ green |
| 18.1-05 T2 | SC-3, D-14, D-21 | PgMySubscription with cancel, PgManageSubscriptionButton, useSubscription, copy | node | `pnpm --dir ../vue-paymentgateway-layer test` (files `tests/subscriptionComponents.test.ts`, `tests/i18n.test.ts`) | ✅ (05 T2, 06 T2) | ✅ green |
| 18.1-05 T3 | D-22 | layer release decision (reply "hold") | checkpoint | n/a (the final push is 18.1-06 T5) | n/a | ✅ recorded |
| 18.1-06 T1 | SC-5 | fail-closed gate with detector self-test | gate | `bash scripts/check-phase18.1.sh --self-test` | ✅ (06 T1) | ✅ green |
| 18.1-06 T2 | SC-5, D-17, D-14 | framework and layer coverage; the `reorder.flat` defect fixed | gate | `bash scripts/check-phase18.1.sh go && bash scripts/check-phase18.1.sh admin && bash scripts/check-phase18.1.sh layers` | ✅ (06 T2) | ✅ green |
| 18.1-06 T3 | SC-5, D-03 to D-16, D-21 | pgsub coverage | gate | `bash scripts/check-phase18.1.sh plugins && bash scripts/check-phase18.1.sh postgres` | ✅ (06 T3) | ✅ green |
| 18.1-06 T4 | SC-5, D-11, D-12, D-15, D-18, D-19 | pgstripe coverage and the PHP map | gate | `PHASE181_REQUIRE_POSTGRES=1 bash scripts/check-phase18.1.sh --all` | ✅ (06 T4) | ✅ green |
| 18.1-06 T5 | D-22 | final push and tag decision | checkpoint | n/a (awaiting the human reply) | n/a | ⬜ pending |
The success-criterion view (seeded by the planner) maps onto the rows above:
| Req | Behaviour | Proven by |
|-----|-----------|-----------|
| SC-1 | baseline up/down, idempotent re-run, names equal the PHP v2.4.0 dump; `users.stripe_customer_id` only when missing; registries; user subscriber; status, grace, feature and renewal matrices | `TestBaselineSchema`, `TestSubscriptionsCoreSmoke`, `TestRegistry`, `TestUserSubscribable`, `TestSubscriptionChecks`, `TestPlanPrices`, `TestFeatureList` |
| SC-2 | webhook answers and URLs; claim-first job (STRIPE-006/007), duplicate and concurrent; event matrix; plan sync | `TestSubscriptionWebhookSmoke`, `TestSubscriptionJob`, `TestApplyEvents`, `TestClaimEventConcurrent`, `TestSyncPlan`, `TestSubscriptionGateway` |
| SC-3 | admin screens and actions; customer API; layer client and components | `TestAdminPlansSmoke`, `TestAdminSubscriptionsSmoke`, `TestAdminSyncActions`, `TestCustomerAPISmoke`, `TestCustomerAPIAuthz`, the layer's `subscriptions:` specs |
| SC-4 | the proof host flow incl. a real conga worker | `TestHostSubscriptionFlow` |
| SC-5 | the PHP test map and the gate | `TestStripeSubscriptionReliability`, `TestSubscriptionAuthentication`, `TestStripeSubscriptionSecrets`, `TestSDKPin`; `scripts/check-phase18.1.sh --all` |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
@@ -62,10 +88,10 @@ Filled in by the planner per task; the success-criterion map below is the contra
## Wave 0 Requirements
- [ ] `sm-paymentgatewaysubscriptions-plugin/testmain_test.go` + harness (copy P18 `testmain_test.go`, `harness_test.go`)
- [ ] Subscription-endpoint Stripe fake usable from `sm-pgstripe-plugin/subscriptions` (P18's `stripefake_test.go` is not importable): customers, checkout sessions, billing portal sessions, subscriptions get/update/cancel, products create/update, prices list/create/update
- [ ] Webhook event-body builders for both API shapes (pre-basil and basil) for subscription and invoice objects
- [ ] `scripts/check-phase18.1.sh`
- [x] `sm-paymentgatewaysubscriptions-plugin/testmain_test.go` + harness (18.1-02)
- [x] Subscription-endpoint Stripe fake in `sm-pgstripe-plugin/subscriptions` (18.1-03; price failure switch added in 18.1-06)
- [x] Webhook event-body builders for both API shapes (`subscriptions/eventbody_test.go`, 18.1-03)
- [x] `scripts/check-phase18.1.sh` (18.1-06)
---
@@ -73,18 +99,19 @@ Filled in by the planner per task; the success-criterion map below is the contra
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Billing-portal cancellation arrives as `cancel_at_period_end` or `cancel_at` | SC-2 | Depends on live Stripe portal behaviour on the account's API version | Stripe test mode + `stripe listen` forwarding to the subscriptions URL; cancel through the portal; inspect the delivered `customer.subscription.updated` |
| Billing-portal cancellation arrives as `cancel_at_period_end` or `cancel_at` | SC-2, D-21 | Depends on live Stripe portal behaviour on the account's API version | Stripe test mode + `stripe listen` forwarding to the subscriptions URL; cancel through the portal; inspect the delivered `customer.subscription.updated` |
| Real Stripe test-mode checkout end to end | SC-4 | The fake cannot prove Stripe's hosted checkout | Test-mode keys in settings, run checkout from the layer playground, confirm activation and the `activated` event |
| Admin screens and layer components in a real browser | SC-3, D-17 | vitest and SSR specs do not drive a browser | Try the taglist chips, the flat drag reorder, the sync toasts, the plans page, Checkout and portal redirects and the cancel dialog once |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 60s
- [ ] `nyquist_compliant: true` set in frontmatter
- [x] All tasks have `<automated>` verify or Wave 0 dependencies
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
- [x] Wave 0 covers all MISSING references
- [x] No watch-mode flags
- [x] Feedback latency < 60s for the quick run
- [x] `nyquist_compliant: true` set in frontmatter
**Approval:** pending
**Approval:** validated 2026-10-14 by the 18.1-06 gate run (`PHASE181_REQUIRE_POSTGRES=1 bash scripts/check-phase18.1.sh --all`, postgres run on a Docker host)

View File

@@ -3,17 +3,20 @@
## Deferred Items
- The localized list schema drops `reorder.flat`
status: open
status: resolved
**Resolved by:** plan 18.1-06, framework commit `0f09586` (`ListSchema.Localize` copies `Flat`); the RED test is `TestReorderFlatCompile/the_localized_list_schema_keeps_flat` (`323b6d6`). The cabana README and `docs/backend/lists-and-filters.md` already described the flag on the list schema, so no doc change was needed.
**Found during:** plan 18.1-04 Task 2 (TestAdminPlansSmoke/flat_reorder).
**What:** `modules/cabana/list_schema.go` builds the localized `ListReorder` with the title and the partition only (around line 618), so `GET .../schema/list` answers `"reorder":{"title":...}` without `"flat":true` for a flat reorder page, although the compiled `ListReorder.Flat` is set and documented as part of the list schema (plan 18.1-01). The reorder page itself is unaffected: the SPA reads `flat` from `GET .../reorder` (`AdminReorderTree.Flat`), which answers it correctly.
**Fix:** copy `Flat` into the localized `ListReorder` and add the assertion to `reorder_flat_test.go`'s HTTP path; a framework change for plan 18.1-06 or a quick task (out of scope for the plugin repository plan 04 commits to).
- The layer's `scripts/check-git-install.sh` targets `vue-journal-layer`
status: open
**Plan 18.1-06:** left open. Neither the phase gate (`scripts/check-phase18.1.sh`) nor the plan's release task runs it, so the release does not depend on it. It is listed on the final push sheet: after a `v0.2.0` tag, an install check of the tag needs this script fixed first (a quick task against the layer).
**Found during:** plan 18.1-05 Task 3 (release sheet).
**What:** `../vue-paymentgateway-layer/scripts/check-git-install.sh`, shipped since v0.1.0 and run by `pnpm run check:git-install <tag>`, was copied from the journal layer. `REPO_URL`, the scratch app's dependency name, `extends` and the messages (12 "journal" references) point at `vue-journal-layer`, and it expects `testing/expect-acme.txt`, which the payment gateway layer does not have. Running `pnpm run check:git-install v0.2.0` would check the wrong repository.
**Fix:** for plan 18.1-06, point the script at `git.golem15.com/golem15/vue-paymentgateway-layer.git` and the package name `vue-paymentgateway-layer`, and replace the journal fixture check with a payment gateway build check. Run it after the `v0.2.0` tag exists.
- Watch: pnpm store-dir mismatch in `vue-paymentgateway-layer`
status: open
status: resolved
**Resolved by:** plan 18.1-06. `scripts/check-phase18.1.sh layers` (and `--all`) ran the layer suite green from a clean shell with the lockfile unchanged. The gate never installs: if pnpm aborts with `ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY`, the layer stage fails and prints the offline, frozen-lockfile recovery command.
**Found during:** plan 18.1-05 Task 2 verify.
**What:** the layer's `node_modules` was installed against the store `/media/nvme/dev/golem15/summercms.io/summercms/.pnpm-store/v11`, but pnpm 11.3.0 now resolves `/media/nvme/.pnpm-store/v11`. After any `package.json` change, `pnpm test` runs `pnpm install` first and aborts without a TTY (`ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY`). Plan 05 fixed the local state with `pnpm install --frozen-lockfile --offline --store-dir /media/nvme/dev/golem15/summercms.io/summercms/.pnpm-store`, which left the lockfile unchanged and added no package.
**Fix:** plan 18.1-06's `scripts/check-phase18.1.sh` (and any other checkout) may hit the same thing. Either pin `store-dir` in the layer's `pnpm-workspace.yaml`, or run the frozen offline install before the layer stage. Close this item once the check script runs the layer suite green from a clean shell.