docs(p15): add G15a CSI static MVP port plan

This commit is contained in:
pingqiu
2026-05-03 07:43:31 -07:00
parent ed1cf4dcfa
commit b5d3cd1ee9
2 changed files with 423 additions and 0 deletions
@@ -0,0 +1,216 @@
# V3 Phase 15 — G15a CSI Static MVP Mini-Plan
**Date**: 2026-05-03
**Status**: architect draft; code must start from this plan, not from direct V2 copy
**Branch**: proposed `p15-g15a/csi-static-mvp` off `origin/phase-15`
**Goal**: make Kubernetes CSI consume an already-provisioned V3 block assignment and mount it through the Linux node path.
---
## §1 Scope
G15a is not dynamic provisioning. It is the first CSI product loop:
```
cluster-spec / product-loop assignment
-> blockmaster publishes target facts
-> CSI ControllerPublish returns publish_context
-> CSI NodeStage logs in / connects and mounts
-> CSI NodePublish bind-mounts into pod path
```
The claim is: **a pre-provisioned V3 volume can be consumed by CSI without CSI minting authority or owning storage**.
---
## §2 V2 Port Discipline
Port V2 execution code where it is mechanism:
| V2 source | Decision | Reason |
|---|---|---|
| `csi/identity.go` | PORT-AS-IS | CSI spec mechanism. |
| `csi/server.go` endpoint/service registration | PORT-REBIND | Keep gRPC/endpoint shape; replace V2 local manager/backend wiring. |
| `csi/node.go` NodeStage/Publish/Unstage core | PORT-REBIND, mostly complete | Linux attach/mount mechanism is valuable and already QA-hardened. Remove local `VolumeManager` fallback for G15a. |
| `csi/iscsi_util.go` | PORT-AS-IS | OS initiator + mount mechanism; injectable for tests. |
| `csi/nvme_util.go` | PORT-AS-IS but optional in G15a | Keep shape; G15a may default to iSCSI-first if NVMe hardware confidence is lower. |
| `csi/controller.go` ControllerPublish/Unpublish/Capabilities/Validate only | PORT-REBIND | Keep CSI request validation and response shape; backend is V3 assignment/status lookup. |
| `csi/volume_backend.go` interface concept | REWRITE-TINY | V3 backend should expose lookup/publish facts from blockmaster, not V2 master RPC. |
| `csi/volume_manager.go` | SKIP | V2 storage/target ownership; violates V3 boundary. |
| snapshots / expand / CreateVolume | SKIP for G15a | Dynamic lifecycle belongs after product API/placement allocation is ratified. |
---
## §3 iSCSI / NVMe / CSI Unification Rule
CSI must not invent target identity. The V3 iSCSI/NVMe frontends already own protocol identity and target serving:
- `cmd/blockvolume --iscsi-listen --iscsi-iqn` starts `core/frontend/iscsi.Target`.
- `cmd/blockvolume --nvme-listen --nvme-subsysnqn` starts `core/frontend/nvme.Target`.
- iSCSI and NVMe target packages own wire/session semantics, VPD/Identify identity, capacity, and backend opening.
CSI is only the Kubernetes attach/mount layer. It consumes a **published frontend target fact**:
```
volume_id
replica_id
authority epoch / endpoint_version
protocol = iscsi | nvme
target address
iqn or subsys_nqn
lun or nsid
capacity / block size (optional read-only evidence)
```
Current V3 gap: `AssignmentFact` carries data/ctrl addresses, and `cmd/blockvolume` prints `iscsi-listening` / `nvme-listening` lines for tests, but the master status API does not yet expose frontend target facts to a CSI controller. G15a must either:
1. add a read-only observation/status path for frontend target facts, or
2. constrain the first L2 CSI smoke to a test backend that receives the same facts from subprocess ready lines.
The product direction is option 1. The test-only bridge in option 2 is allowed only as a temporary pre-product slice and must be named as such.
Boundary rules:
- CSI package may import CSI spec, OS attach helpers, and a narrow V3 publish-target lookup interface.
- CSI package must not import `core/authority`.
- CSI package must not construct `AssignmentAsk`, `AssignmentFact`, `adapter.AssignmentInfo`, Epoch, or EndpointVersion.
- CSI package must not derive IQN/NQN from volume ID when a publish target fact is available.
- IQN/NQN derivation, if needed for defaults, belongs in one shared helper used by `cmd/blockvolume` and CSI tests, not duplicated independently.
Loopback note:
Current `cmd/blockvolume` rejects non-loopback iSCSI/NVMe binds because frontends are unauthenticated. G15a has two valid modes:
- same-node/static MVP: CSI node plugin connects to a loopback target on the same node;
- real multi-node Kubernetes: requires a separate security/bind ratification before exposing iSCSI/NVMe on routable addresses.
Do not silently turn off the loopback guard inside CSI.
---
## §4 Red Tests
The first code slice must land tests before or with implementation:
1. `ControllerPublish` fails closed when the V3 assignment/status backend has no verified frontend target for `volume_id`.
2. `ControllerPublish` returns `iscsiAddr` + `iqn` from V3 assignment/status facts, not from cluster-spec directly.
3. CSI package does not import `core/authority` and does not call `Publisher` / mint assignment facts.
4. `NodeStageVolume` uses `PublishContext` before `VolumeContext`.
5. `NodeStageVolume` is idempotent when the staging path is already mounted.
6. `NodeStageVolume` cleans up login/connect if format/mount fails.
7. `NodeUnstageVolume` preserves staged state on unmount/logout failure for retry.
8. `.transport` restart recovery picks the correct disconnect path and rejects garbage transport values.
9. `NodePublishVolume` bind-mounts staging to target and is idempotent.
10. `CreateVolume`, snapshots, and expand return `Unimplemented` or are not advertised in G15a capabilities.
11. CSI publish target lookup uses the same IQN/NQN as `cmd/blockvolume` frontend flags / frontend target facts.
12. CSI attach must fail closed if the current authority target has no frontend target fact yet.
13. CSI must not bypass the current authority line by using a stale endpoint from `VolumeContext`.
---
## §5 Slices
### G15a-1 — CSI package skeleton + boundary guards
Code:
- New V3 CSI package, likely `core/csi` or `core/frontend/csi`.
- Identity service.
- Endpoint parser and service registration.
- Controller capability surface with dynamic operations omitted.
Tests:
- Identity nil request behavior.
- endpoint parse tests.
- no authority import / no publisher call guard.
- Controller capabilities advertise only G15a-supported operations.
### G15a-2 — Node iSCSI attach/mount mechanism
Code:
- Port V2 `node.go` iSCSI path and `iscsi_util.go` / mount helpers.
- Remove local V2 manager fallback.
- Keep `PublishContext` > `VolumeContext`.
- Keep staged map, `.transport`, cleanup, idempotency.
Tests:
- Port V2 node tests that do not rely on `VolumeManager`.
- Port V2 adversarial cleanup tests.
### G15a-3 — ControllerPublish V3 backend
Code:
- Define V3 CSI backend interface: `LookupPublishTarget(ctx, volumeID, nodeID)`.
- Implement read-only backend against current blockmaster status / assignment surface plus frontend target facts, or a testable adapter around the existing product-loop state.
- Return `publish_context` with iSCSI first; NVMe optional.
Tests:
- found/missing target.
- no direct cluster-spec shortcut.
- no authority minting.
- target fact must match the frontend fact reported by blockvolume.
### G15a-4 — L2 subprocess CSI smoke
Code:
- `cmd/blockcsi` or `cmd/block-csi` binary.
- Flags: endpoint, mode, node-id, master/status address, default transport.
Test:
- blockmaster + blockvolume + CSI controller/node subprocess.
- ControllerPublish gets target facts.
- NodeStage/Publish uses mockable or real privileged path depending environment.
- Non-privileged CI can stop at fake `ISCSIUtil` / `MountUtil`; M01/k8s executes real path.
### G15a-5 — Kubernetes YAML skeleton
Code/docs:
- Port V2 deploy YAML with V3 image/flags/driver name.
- Static PV/PVC example for pre-provisioned volume.
Test:
- QA instruction only unless k8s lab is available.
---
## §6 Non-Claims
G15a does not claim:
- dynamic CSI provisioning (`CreateVolume`);
- snapshot or expansion support;
- replica auto-repair under Kubernetes faults;
- pod remount after primary kill;
- multi-node network partition behavior;
- NVMe as the default attach path.
Those belong to G15b/G15c after the static CSI path is stable.
---
## §7 QA Instructions
Default CI:
```powershell
go test ./core/csi ./cmd/blockcsi ./cmd/blockvolume ./cmd/blockmaster -count=1
```
If the package/binary names differ, update this section in the first implementation commit.
M01 / privileged Linux:
1. Start blockmaster with cluster-spec.
2. Start blockvolume primary with iSCSI enabled.
3. Start CSI controller/node.
4. Run CSI ControllerPublish for the existing volume.
5. Run NodeStage/NodePublish.
6. Write/read through mounted filesystem.
7. NodeUnpublish/NodeUnstage must clean up mount and session.
---
## §8 Forward-Carry
- G15b: dynamic `CreateVolume` backed by desired-volume + placement allocation.
- G15c: Kubernetes node down/up, pod reschedule, primary kill, replica catch-up/rebuild, and data continuity.
- G15d: NVMe attach path as first-class CSI transport.
- G15e: snapshots/expand once V3 product APIs exist.
@@ -0,0 +1,207 @@
# V3 Phase 15 — G15a (CSI MVP) port survey
**Date**: 2026-04-28; code-read addendum 2026-05-03
**Status**: Layer 1 topology + 2026-05-03 V2 code-read addendum complete; G15a scope not yet ratified
**Owner**: QA (read-only port-survey assignment per architect 2026-04-28 ratification)
**Scope**: pre-ratification scoping only; not a code commitment; per `feedback_porting_discipline.md`
**Methodology — top-down**:
- Sources: directory listings, V2 design doc `v3-csi-iscsi-nvme-block-overview.md`, repo memory, k8s CSI v1.5 spec
- Goal: identify high-level modules + dependency graph, classify port disposition at module granularity
- Anti-pattern guard: Layer 1 did not open V2 `.go` files. The 2026-05-03 addendum below does open selected V2 `.go` files and records actual port disposition.
---
## Layer 1 — V2 CSI module topology
### V2 code locations (inventory only)
| Path | Contents |
|---|---|
| `weed/storage/blockvol/csi/` | 9 `.go` source files + 9 `_test.go` files + `cmd/` + `deploy/` |
| `weed/storage/blockvol/csi/cmd/block-csi/` | Driver binary entry point |
| `weed/storage/blockvol/csi/deploy/` | 6 k8s YAMLs (controller, node, driver, rbac, storageclass, example-pvc) |
| `weed/storage/blockvol/testrunner/` | Test runner platform: engine, agent, coordinator, baseline; `actions/` (k8s, iscsi, block, io, metrics, fault); `infra/` (target, ha_target, iscsi_client, node, artifacts, fault) |
| `weed/storage/blockvol/testrunner/scenarios/` | 16+ YAML scenarios incl. `coord-smoke-iscsi`, `coord-ha-failover`, `cp103-nvme-*`, `cp83-snapshot-expand`, `cp84-soak-4h`, `cp85-chaos-disk-full` |
### Top-level modules (Layer 1)
Seven distinct modules, classified by their primary coupling:
| # | Module | What it does (1-line) | Primary coupling |
|---|---|---|---|
| 1 | **CSI gRPC service layer** (controller.go, node.go, identity.go, server.go) | Implements k8s CSI v1.5 spec gRPC services: Controller / Node / Identity | k8s CSI spec (protobuf) |
| 2 | **Transport adapter layer** (iscsi_util.go, nvme_util.go) | OS-level orchestration of iSCSI initiator (iscsiadm) and NVMe-oF initiator (nvme-cli); device-path resolution; format/mount glue | OS / Linux kernel |
| 3 | **Volume backend bridge** (referenced as `VolumeBackend` in design doc, called from controller.go) | Talks to V2 master to create/delete BlockVol; receives IQN/NQN + addresses to put in PublishContext | **V2 wire** ← THIS IS THE PORT EDGE |
| 4 | **Snapshot ID encoding** (snapshot_ids.go) | Encodes/decodes snapshot identifiers across CSI/backend boundary | Internal Go interface |
| 5 | **Main binary** (cmd/block-csi/) | Wires gRPC server + transport adapters + volume backend; CLI flags | Internal wiring |
| 6 | **K8s deployment artifacts** (deploy/) | k8s YAMLs: csi-controller, csi-node, csi-driver, rbac, storageclass, example-pvc | k8s API |
| 7 | **Test corpus** (`*_test.go` + testrunner YAML scenarios) | In-package unit tests (controller, node, identity, snapshot, qa_*) + sw-test-runner end-to-end YAML scenarios driving CSI gRPC + iSCSI/NVMe device flows in a real (or simulated) cluster | Mixed (some k8s-spec, some V2-wire, some OS) |
### Dependency graph
```
┌──────────────────┐
│ cmd/block-csi │ (entrypoint, wires everything)
└────────┬─────────┘
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌────────────────┐ ┌──────────────────┐
│ gRPC svc │ │ Volume backend │ │ Transport │
│ (Controller │──▶│ bridge │ │ adapters │
│ Node │ │ (★ V2 wire ★) │ │ (iSCSI / NVMe) │
│ Identity) │ └────────┬───────┘ └────────┬─────────┘
└──────┬──────┘ │ │
│ ▼ ▼
▼ ┌──────────────┐ ┌──────────────┐
k8s CSI spec │ V2 master / │ │ OS / kernel │
(protobuf) │ volume gRPC │ │ (iscsiadm, │
└──────────────┘ │ nvme cli, │
│ mount) │
└──────────────┘
(deployment artifacts: (snapshot IDs:
deploy/*.yaml, pure Go,
k8s API only) no external coupling)
```
**Key observations:**
- Module 3 (Volume backend bridge) is the **only** module with hard V2 wire coupling. Everything else couples to k8s CSI spec or OS — both stable across V2/V3.
- Modules 1, 2, 4 are protocol/spec-bound and don't know V2 vs V3 exists.
- Module 6 (deployment YAMLs) couples only to driver name + image name — trivial edits.
- Module 7 (tests) splits along the same lines as the modules they exercise: protocol-level tests (most) are V2/V3-agnostic; wire-level QA tests (`qa_cp62`, `qa_cp83`) need rewire matching new backend.
### Layer 1 disposition (preliminary — Layer 4 will refine)
| # | Module | Preliminary disposition | Why |
|---|---|---|---|
| 1 | CSI gRPC service layer | **port-as-is** | k8s CSI v1.5 spec is stable; gRPC handlers shape doesn't change |
| 2 | Transport adapter layer | **port-as-is** | OS/kernel interface unchanged; iscsiadm + nvme-cli identical between V2/V3 |
| 3 | Volume backend bridge | **port-with-rewire** | Same shape (CreateVolume → backend, DeleteVolume → backend), but the backend interface implementation must be replaced to call V3 master/replication APIs instead of V2 master |
| 4 | Snapshot ID encoding | **port-as-is** | Pure Go encoding, no external deps |
| 5 | Main binary | **port-with-light-rewrite** | Wiring code: same module composition, new flag set possibly; minor edits |
| 6 | K8s deployment artifacts | **port-with-light-rewrite** | Update image name, driver name, possibly RBAC for V3-specific service accounts; YAML structure unchanged |
| 7 | Test corpus | **mixed** — split per Layer 2 | Most tests follow their module's disposition; QA wire-level tests need rewire |
### Layer 1 estimated split (rough)
- **port-as-is**: ~50-60% of LOC (modules 1, 2, 4 + most of 7)
- **port-with-light-rewrite**: ~10-15% (modules 5, 6 + a few tests)
- **port-with-rewire**: ~25-35% (module 3 + wire-level QA tests in 7)
- **rewrite-required**: should be near 0% — would surface in Layer 4 if found
### Layer 1 cross-checks (sanity)
| Cross-check | Result |
|---|---|
| **Architect's discipline** ("port from V2 is heavy reuse, not rewrite") | ✅ consistent — Layer 1 estimates ~75% port-as-is or light-rewrite |
| **NVMe support preservation** (user's concern, 2026-04-28) | ✅ Module 2 includes `nvme_util.go`; transport adapter layer is multi-protocol by design |
| **CSI v1.5 spec stability across V2/V3** | ✅ k8s CSI spec is independent of any storage backend |
| **Existing k8s e2e scenarios reusable** | ✅ 16+ YAML scenarios in testrunner/scenarios/ exercise full CSI flows; most should port-as-is |
### Layer 1 open questions (deferred to Layer 2-5)
1. **Volume backend interface** (Module 3): how thin is the V2-wire surface? If it's ≤10 method calls into V2 master, port is mechanical. If it's deeply intertwined with V2 master state shapes, may need redesign of the interface.
2. **NodeStageVolume flow**: how is "prefer NVMe over iSCSI" implemented today? Is it config or auto-detect? V3 needs the same flexibility.
3. **Snapshot ID semantics**: Module 4 ID encoding may assume V2 snapshot model; V3 snapshot lifecycle may differ when G10/G15b lands.
4. **Test corpus split**: of the 9 `_test.go` files, how many touch V2 wire vs are pure protocol/encoding tests? Affects port effort estimate.
---
## 2026-05-03 code-read addendum — actual V2 CSI port disposition
Selected V2 files read:
| V2 file | Finding | Disposition for V3 |
|---|---|---|
| `csi/controller.go` | CSI Controller service shape is spec-bound: create/delete/publish/unpublish/capability/expand/snapshot handlers translate CSI requests into a `VolumeBackend` interface. | **M / port selective**. Keep CSI request validation, idempotency, `VolumeContext` / `PublishContext` shape. Do not initially port snapshots/expand into G15a static MVP. |
| `csi/node.go` | NodeStage/Publish/Unstage uses `PublishContext` / `VolumeContext`, chooses NVMe if available, otherwise iSCSI, runs OS initiator commands, formats/mounts, persists `.transport`, and is idempotent on already-mounted paths. | **M / port complete for Node path**. This is the first useful CSI MVP surface because it tests real Linux attach/mount/read/write. Rebind imports and keep OS utilities injectable for tests. |
| `csi/iscsi_util.go` | Pure OS adapter: `iscsiadm`, device discovery under `/dev/disk/by-path`, mount/mkfs/resize helpers, mock utilities for unit tests. | **M / port complete**. This is directly reusable for Linux node plugin. |
| `csi/nvme_util.go` | Pure OS adapter: `nvme connect/disconnect/list-subsys/ns-rescan`, JSON parsing, mock utilities. | **M / port complete but G15a may default iSCSI-first**. NVMe is reusable after frontend/NVMe m01 confidence is high. |
| `csi/identity.go` | CSI Identity service constants and readiness. | **M / port complete**. Minimal and safe. |
| `csi/server.go` | Wires CSI Identity/Controller/Node services and local V2 `VolumeManager` / master client modes. | **M* / port shell only**. Keep endpoint parsing and service registration; replace local manager/master backend with V3-specific backends. |
| `csi/volume_backend.go` | Abstract `VolumeBackend`, local V2 `VolumeManager` backend, and V2 master gRPC client. | **M* + E split**. Keep the interface shape concept; do not port V2 master client. V3 backend must read from cluster-spec/product-loop assignment facts or a future blockmaster volume API. |
| `csi/volume_manager.go` | Embeds V2 `BlockVol` and V2 iSCSI `TargetServer`; creates local `.blk` files and exposes them. | **E / do not port** for V3 product mode. V3 blockvolume already owns frontend/export and durable storage. |
| `csi/cmd/block-csi/main.go` | CLI entry point for CSI driver. | **M* / rewrite-thin**. Keep flag style and signal handling; wire V3 backend and no local V2 manager. |
| `csi/deploy/*.yaml` | Kubernetes CSI controller/node/driver/rbac/storageclass/example PVC. | **M* / port with image/name/flag edits**. Useful as G15a deployment skeleton after binary exists. |
### Key architecture conclusion
V2 CSI should not be ported as "create volume + embedded BlockVol + local target" for V3. That path would bypass the P15 product loop we just built.
For V3 the first CSI gate should be:
```
cluster-spec / existing placement
-> blockmaster product loop
-> authority assignment
-> blockvolume exposes iSCSI/NVMe
-> CSI ControllerPublish returns publish_context
-> CSI NodeStage attaches OS initiator and mounts
-> pod writes filesystem
```
This tests the real V3 control/data boundary without reintroducing V2 storage ownership into the CSI driver.
### Recommended G15a split
| Slice | Claim | Code scope | Test scope |
|---|---|---|---|
| G15a-0 CSI service skeleton | CSI binary starts; Identity works; Controller/Node capability shape correct; no authority minting. | New `cmd/blockcsi` or `cmd/block-csi`; `core/csi` package; Identity; endpoint parsing; mocks. | Unit tests only. |
| G15a-1 static/pre-provisioned iSCSI Node path | Given publish_context from an existing V3 blockvolume assignment, CSI NodeStage/Publish can attach, format, mount, bind, unpublish, unstage. | Port V2 `node.go`, `iscsi_util.go`, mount helpers; no CreateVolume. | Mock unit tests + optional privileged M01 test. |
| G15a-2 ControllerPublish from blockmaster assignment | CSI ControllerPublish/Validate can look up existing volume target info from V3 blockmaster/status and return `iscsiAddr` + `iqn`. | V3 `VolumeBackend` backed by blockmaster query/status, not V2 master RPC. | L2 subprocess with real blockmaster + blockvolume; no Kubernetes yet. |
| G15a-3 Kubernetes smoke | K8s static PV/PVC + CSI node plugin can mount a V3 iSCSI volume and read/write. | Deploy YAMLs; container/image flags; node privileged mounts. | M01 or k8s lab only. |
| G15b dynamic provisioning | CSI CreateVolume creates desired volume + placement intent, waits for assignment readiness, then returns volume. | New product API for desired volume creation and placement allocation. | Requires G9/G10 product-loop maturity; not G15a. |
| G15c fault-domain validation | Node down/up, network partition, replica repair, pod remount/recover. | Mostly harness + policy, not CSI-only. | Hardware/k8s failure matrix. |
### Red-test anchors for G15a
1. `ControllerPublish` must fail closed if blockmaster has no verified assignment for the requested volume.
2. `ControllerPublish` must return publish_context from authority/product assignment facts, not from cluster-spec directly.
3. `NodeStageVolume` must be idempotent when the staging path is already mounted.
4. `NodeStageVolume` must clean up iSCSI/NVMe sessions if format/mount fails.
5. `NodeUnstageVolume` must recover transport identity from `.transport` after CSI plugin restart.
6. `NodePublishVolume` must bind-mount staging to pod target and be idempotent.
7. CSI package must not import `core/authority` or mint assignments.
8. Dynamic `CreateVolume` must be explicitly unimplemented in G15a unless a V3 product API is ratified.
### What this means for "real domain" testing
CSI is the right next user-facing integration layer, but it should not be asked to prove every recovery property at once.
- **G15a proves**: Kubernetes/node attach path can consume existing V3 assignment and mount a real block device.
- **G15c/G16 prove**: node down/up, network interruption, replica auto-repair, remount behavior, and pod-level data continuity.
- **G9/G10 continue to own**: placement, readiness, recovery decisions, and authority minting. CSI consumes those facts; it does not decide them.
## Layer 2-5 — superseded by 2026-05-03 addendum; original next-step notes retained
Per `v3-batch-process.md` discipline + architect 2026-04-28 ratification: Layer 1 (this section) is **read-only scoping** allowed without commitment. Layers 2-5 require the dogfood spine ratification before further work, since the "should we port at all + when" question depends on the schedule architect picks.
If/when ratified, Layer 2-5 plan:
- **Layer 2** (sub-modules + call graph within each Layer 1 module): 1 hour
- **Layer 3** (interface inventory per sub-module): 1 hour
- **Layer 4** (per-interface disposition: port-as-is / port-with-rewire / port-with-light-rewrite / rewrite-required / n/a): 30 min
- **Layer 5** (open V2 code — first time — and verify Layer 1-4 against actual implementation): 1-2 hours
- **Total** Layer 2-5: ~3-4 hours QA work
### Methodology guard for Layer 2-5
Per `feedback_porting_discipline.md`:
- No "顺便重构" (incidental refactor) during port
- No rewrite based on "this code looks unfamiliar"
- Rewrite-required disposition (Layer 4) requires explicit architect sign-off, not QA judgment alone
---
## Survey output for sw + architect at G15a kickoff
When G15a §1.A scope is being bound, this survey provides:
1. **Module count + dependency graph** (Layer 1 — already in this doc)
2. **Volume backend interface surface** (Layer 3 — TBD)
3. **Per-interface port disposition** (Layer 4 — TBD)
4. **LOC + test count estimate** (Layer 5 — TBD)
5. **NVMe + iSCSI dual-transport coverage assertion** (Layer 4)
6. **Reusable test scenarios from testrunner/scenarios/** (Layer 4)
These six artifacts let architect bind G15a scope without sw guessing port effort.