# Standard WCS Core Foundation Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 建立可被 `ruoyi-admin` 依赖的 `ruoyi-wcs` 模块,并交付订单、流程、步骤、幂等和核心持久化边界。 **Architecture:** `ruoyi-wcs` 只暴露入站用例、出站端口和查询 DTO;任务状态迁移保存在领域对象中,应用服务负责事务和幂等。Admin 只增加模块依赖,本计划不接 PLC,也不替换现有调度线程。 **Tech Stack:** Java 8, Spring Boot 2.5.15, Maven, JUnit 5, ArchUnit 1.2.1, MyBatis, SQL Server. ## Global Constraints - 根 Reactor 新增且只新增 `ruoyi-wcs` 产品模块。 - `ruoyi-wcs` 只依赖 `ruoyi-common`、Spring 事务、MyBatis 和测试依赖;不依赖 `ruoyi-admin`、PLC 库或项目包。 - 所有公共接口使用 Java 8 类型,不使用 record、sealed class 或高版本 API。 - 订单、步骤和事件 ID 使用非空 `String`,时间使用 `java.time.Instant`。 - 外部请求以 `externalRequestId` 幂等;重复请求返回同一 `orderId`。 - 终态订单和步骤不可反向迁移。 - 本计划不修改 `BaseSRgvThread`、`PlcSrgvRead`、现有 WMS Controller 或旧业务表。 --- ### Task 1: Add the ruoyi-wcs Maven module and architecture test harness **Files:** - Modify: `pom.xml` - Modify: `ruoyi-admin/pom.xml` - Create: `ruoyi-wcs/pom.xml` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/WcsModule.java` - Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/architecture/WcsModuleArchitectureTest.java` **Interfaces:** - Produces: Maven artifact `com.ruoyi:ruoyi-wcs:${ruoyi.version}`. - Produces: marker type `com.ruoyi.wcs.WcsModule`. - Enforces: no class under `com.ruoyi.wcs..` depends on `com.ruoyi.schedule..`, `com.ruoyi.api..`, `HslCommunication..`, or `org.apache.plc4x..`. - [ ] **Step 1: Add the module skeleton and a deliberately failing boundary test** Add `ruoyi-wcs` after `ruoyi-common`, add the managed dependency, add the Admin dependency, and create this test before `WcsModule` exists: ```java @Test void exposesAStableModuleMarker() throws Exception { assertEquals("com.ruoyi.wcs.WcsModule", Class.forName("com.ruoyi.wcs.WcsModule").getName()); } ``` Configure `spring-boot-starter-test` and `archunit-junit5:1.2.1` as test dependencies in `ruoyi-wcs/pom.xml`; configure Surefire `3.2.5` in the root build. - [ ] **Step 2: Run the module test and observe RED** Run: `mvn -pl ruoyi-wcs -am -Dtest=WcsModuleArchitectureTest test` Expected: FAIL because `com.ruoyi.wcs.WcsModule` does not exist. - [ ] **Step 3: Add the marker and dependency rules** ```java package com.ruoyi.wcs; public final class WcsModule { private WcsModule() { } } ``` Add ArchUnit rules: ```java @Test void coreDoesNotDependOnProjectOrPlcPackages() { noClasses().that().resideInAPackage("com.ruoyi.wcs..") .should().dependOnClassesThat() .resideInAnyPackage("com.ruoyi.schedule..", "com.ruoyi.api..", "HslCommunication..", "org.apache.plc4x..") .check(new ClassFileImporter().importPackages("com.ruoyi.wcs")); } ``` - [ ] **Step 4: Run module and Admin reactor tests GREEN** Run: `mvn -pl ruoyi-wcs,ruoyi-admin -am test` Expected: Reactor succeeds and Surefire reports at least one `ruoyi-wcs` test. - [ ] **Step 5: Commit** ```bash git add pom.xml ruoyi-admin/pom.xml ruoyi-wcs git commit -m "build: add standard WCS core module" ``` ### Task 2: Define shared time, ID, event, and failure contracts **Files:** - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/WcsClock.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/IdGenerator.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/DomainEvent.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/DomainRuleViolation.java` - Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/shared/SharedContractsTest.java` **Interfaces:** - Produces: `WcsClock.now(): Instant`. - Produces: `IdGenerator.nextId(): String`. - Produces: `DomainEvent.getEventId/getAggregateId/getSequence/getOccurredAt/getType`. - [ ] **Step 1: Write failing immutable event tests** ```java @Test void rejectsBlankEventIdentity() { assertThrows(IllegalArgumentException.class, () -> new DomainEvent("", "ORDER-1", 1L, Instant.EPOCH, "OrderAccepted")); } @Test void exposesDeterministicEventFields() { DomainEvent event = new DomainEvent("E-1", "ORDER-1", 2L, Instant.parse("2026-08-04T00:00:00Z"), "OrderPlanned"); assertEquals(2L, event.getSequence()); assertEquals("OrderPlanned", event.getType()); } ``` - [ ] **Step 2: Run RED** Run: `mvn -pl ruoyi-wcs -am -Dtest=SharedContractsTest test` Expected: compilation fails because the shared contracts do not exist. - [ ] **Step 3: Implement the minimal contracts** ```java public interface WcsClock { Instant now(); } public interface IdGenerator { String nextId(); } public final class DomainRuleViolation extends RuntimeException { public DomainRuleViolation(String message) { super(message); } } ``` Implement `DomainEvent` as a final immutable class with constructor validation and getters. - [ ] **Step 4: Run GREEN** Run: `mvn -pl ruoyi-wcs -am -Dtest=SharedContractsTest test` - [ ] **Step 5: Commit** ```bash git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/shared git commit -m "feat(wcs): add shared domain contracts" ``` ### Task 3: Implement the TransportOrder state machine **Files:** - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrderState.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrderType.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrder.java` - Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task/TransportOrderTest.java` **Interfaces:** - Produces: `TransportOrder.accept(...)` factory. - Produces: `plan(String workflowId)`, `reject(String code,String reason)`, `start()`, `suspend(String reason)`, `resumeToPlanned()`, `resumeToExecuting()`, `requestCancel()`, `cancel()`, `complete()`, `fail(String reason)`. - Produces: `getUncommittedEvents(): List` and `clearUncommittedEvents()`. - [ ] **Step 1: Write failing legal and illegal transition tests** ```java @Test void completesOnlyThroughPlannedAndExecuting() { TransportOrder order = order(); order.plan("WF-1"); order.start(); order.complete(); assertEquals(TransportOrderState.COMPLETED, order.getState()); } @Test void terminalOrderCannotResume() { TransportOrder order = order(); order.plan("WF-1"); order.cancel(); assertThrows(DomainRuleViolation.class, order::resumeToPlanned); } ``` - [ ] **Step 2: Run RED** Run: `mvn -pl ruoyi-wcs -am -Dtest=TransportOrderTest test` - [ ] **Step 3: Implement explicit transition guards** ```java public void start() { requireState(TransportOrderState.PLANNED); transitionTo(TransportOrderState.EXECUTING, "OrderStarted"); } public void requestCancel() { if (state != TransportOrderState.EXECUTING && state != TransportOrderState.SUSPENDED) { throw new DomainRuleViolation("only executing or suspended order can request cancellation"); } transitionTo(TransportOrderState.CANCEL_REQUESTED, "OrderCancelRequested"); } ``` Implement this exact transition set and increment event sequence exactly once per successful transition: - `ACCEPTED -> PLANNED/REJECTED/SUSPENDED/CANCELLED/FAILED`. - `PLANNED -> EXECUTING/SUSPENDED/CANCELLED/FAILED`. - `EXECUTING -> COMPLETED/SUSPENDED/CANCEL_REQUESTED/FAILED`. - `SUSPENDED -> PLANNED/EXECUTING/CANCEL_REQUESTED/FAILED`. - `CANCEL_REQUESTED -> CANCELLED/FAILED` only after application-level command and resource convergence checks. - `REJECTED/COMPLETED/CANCELLED/FAILED` are mutually exclusive terminal states and reject every further transition. - [ ] **Step 4: Run state tests GREEN** Run: `mvn -pl ruoyi-wcs -am -Dtest=TransportOrderTest test` - [ ] **Step 5: Commit** ```bash git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task git commit -m "feat(wcs): add transport order state machine" ``` ### Task 4: Implement WorkflowInstance and TaskStep dependencies **Files:** - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TaskStepState.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TaskStepType.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TaskStep.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowInstance.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowDefinition.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowStepDefinition.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WorkflowDefinitionProvider.java` - Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task/WorkflowInstanceTest.java` **Interfaces:** - Produces: `WorkflowDefinitionProvider.definitionFor(TransportOrderType): WorkflowDefinition`. - Produces: `WorkflowInstance.create(String workflowId, String orderId, WorkflowDefinition)`. - Produces: `readySteps(): List` and `onStepSucceeded(String stepId)`. - `TaskStep` exposes `markReady`, `block(reasonCode)`, `unblock`, `assign(vehicleId,reservationIds)`, `start`, `suspend(reason)`, `resumeAssigned`, `resumeRunning`, `succeed`, `skip`, `cancel`, and `fail(reason)`. - [ ] **Step 1: Write failing dependency and optional-step tests** ```java @Test void exposesOnlyStepsWhoseMandatoryDependenciesSucceeded() { WorkflowInstance workflow = workflow("MOVE", "DROP"); assertEquals(Collections.singletonList("MOVE"), ids(workflow.readySteps())); workflow.step("MOVE").assign("VEH-1"); workflow.step("MOVE").start(); workflow.step("MOVE").succeed(); workflow.onStepSucceeded("MOVE"); assertEquals(Collections.singletonList("DROP"), ids(workflow.readySteps())); } ``` - [ ] **Step 2: Run RED** Run: `mvn -pl ruoyi-wcs -am -Dtest=WorkflowInstanceTest test` - [ ] **Step 3: Implement dependency evaluation and transition methods** ```java public List readySteps() { List result = new ArrayList(); for (TaskStep step : steps.values()) { if (step.isWaiting() && mandatoryDependenciesSucceeded(step)) { step.markReady(); result.add(step); } } return Collections.unmodifiableList(result); } ``` Implement mandatory dependency failure as `FAILED`, optional branch exclusion as `SKIPPED`, and these exact transitions: `WAITING -> READY/BLOCKED/SKIPPED/CANCELLED/FAILED`; `BLOCKED -> READY/CANCELLED/FAILED`; `READY -> ASSIGNED/BLOCKED/CANCELLED/FAILED`; `ASSIGNED -> RUNNING/SUSPENDED/CANCELLED/FAILED`; `RUNNING -> SUCCEEDED/SUSPENDED/CANCELLED/FAILED`; `SUSPENDED -> ASSIGNED/RUNNING/CANCELLED/FAILED`. `SKIPPED/CANCELLED/FAILED/SUCCEEDED` are terminal. - [ ] **Step 4: Run GREEN** Run: `mvn -pl ruoyi-wcs -am -Dtest=WorkflowInstanceTest,TransportOrderTest test` - [ ] **Step 5: Commit** ```bash git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WorkflowDefinitionProvider.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task git commit -m "feat(wcs): add workflow and task step model" ``` ### Task 5: Add standard order use cases and idempotent application service **Files:** - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/in/TransportOrderUseCase.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task/CreateTransportOrderCommand.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task/TransportOrderResult.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task/TransportOrderApplicationService.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/TransportOrderRepository.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DomainEventPublisher.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WmsNotificationGateway.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WmsNotification.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WmsNotificationReceipt.java` - Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/task/TransportOrderApplicationServiceTest.java` **Interfaces:** - Produces: `create(CreateTransportOrderCommand)`, `cancel(String externalRequestId)`, `suspend(String orderId,String reason)`, `resume(String orderId)`. - Consumes: `TransportOrderRepository.findByExternalRequestId/save`. - Consumes: `WorkflowDefinitionProvider` and `DomainEventPublisher.publish(List)`. - Produces outbound port `WmsNotificationGateway.notify(WmsNotification): WmsNotificationReceipt`; later plans deliver it through a core-owned Outbox. - `WmsNotification` contains `eventId`, `externalRequestId`, `orderId`, `occurredAt`, `eventType`, `resultCode`, `resultMessage`, and typed context fields for `OrderAccepted`, `OrderStarted`, `OrderCompleted`, `OrderFailed`, `OrderCancelled`, and `OrderSuspended`. - [ ] **Step 1: Write failing duplicate-request test** ```java @Test void returnsTheExistingOrderForTheSameExternalRequest() { TransportOrderResult first = service.create(command("REQ-1")); TransportOrderResult second = service.create(command("REQ-1")); assertEquals(first.getOrderId(), second.getOrderId()); assertEquals(1, repository.count()); } ``` - [ ] **Step 2: Run RED** Run: `mvn -pl ruoyi-wcs -am -Dtest=TransportOrderApplicationServiceTest test` - [ ] **Step 3: Implement transactional orchestration** ```java @Transactional public TransportOrderResult create(CreateTransportOrderCommand command) { TransportOrder existing = repository.findByExternalRequestId(command.getExternalRequestId()); if (existing != null) { return TransportOrderResult.from(existing); } TransportOrder order = TransportOrder.accept(command, idGenerator.nextId(), clock.now()); WorkflowDefinition definition = workflowDefinitions.definitionFor(order.getType()); WorkflowInstance workflow = WorkflowInstance.create(idGenerator.nextId(), order.getOrderId(), definition); order.plan(workflow.getWorkflowId()); repository.save(order, workflow); eventPublisher.publish(order.getUncommittedEvents()); order.clearUncommittedEvents(); return TransportOrderResult.from(order); } ``` Implement cancel/suspend/resume using domain methods; do not expose repository entities in results. Map order domain events to immutable `WmsNotification` values without calling the gateway in the order transaction. `WmsNotificationReceipt` exposes `DELIVERED`, `RETRYABLE_FAILURE`, and `PERMANENT_FAILURE`; the notification `eventId` is always the external idempotency key. - [ ] **Step 4: Run GREEN** Run: `mvn -pl ruoyi-wcs -am -Dtest='TransportOrder*Test,WorkflowInstanceTest' test` - [ ] **Step 5: Commit** ```bash git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task ruoyi-wcs/src/main/java/com/ruoyi/wcs/port ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/task git commit -m "feat(wcs): add idempotent transport order use cases" ``` ### Task 6: Add the foundation SQL schema and MyBatis repository **Files:** - Create: `sql/wcs/V001__wcs_task_foundation.sql` - Create: `sql/wcs/verify_V001__wcs_task_foundation.sql` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRow.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/WorkflowRow.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TaskStepRow.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderMapper.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/MyBatisTransportOrderRepository.java` - Create: `ruoyi-wcs/src/main/resources/mapper/wcs/task/TransportOrderMapper.xml` - Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRowMapperTest.java` **Interfaces:** - Implements: `TransportOrderRepository` from Task 5. - Produces: unique constraint `uk_wcs_transport_order_external_request`. - Produces: optimistic `state_version` on order, workflow, and step rows. - [ ] **Step 1: Write a failing domain-row round-trip test** ```java @Test void preservesOrderStateAndVersionAcrossPersistenceMapping() { TransportOrder source = executingOrder("ORDER-1", 4L); TransportOrderRow row = TransportOrderRow.fromDomain(source); TransportOrder restored = row.toDomain(); assertEquals(source.getState(), restored.getState()); assertEquals(4L, restored.getStateVersion()); } ``` - [ ] **Step 2: Run RED** Run: `mvn -pl ruoyi-wcs -am -Dtest=TransportOrderRowMapperTest test` - [ ] **Step 3: Create idempotent SQL Server DDL** The first table must include this exact invariant: ```sql IF OBJECT_ID('dbo.wcs_transport_order', 'U') IS NULL BEGIN CREATE TABLE dbo.wcs_transport_order ( order_id varchar(64) NOT NULL PRIMARY KEY, external_request_id varchar(128) NOT NULL, order_type varchar(32) NOT NULL, load_id varchar(128) NULL, source_code varchar(128) NOT NULL, destination_code varchar(128) NOT NULL, priority int NOT NULL, order_state varchar(32) NOT NULL, workflow_id varchar(64) NULL, state_version bigint NOT NULL, created_time datetime2(3) NOT NULL, updated_time datetime2(3) NOT NULL, CONSTRAINT uk_wcs_transport_order_external_request UNIQUE (external_request_id) ); END; ``` Add `wcs_workflow_instance`, `wcs_task_step`, `wcs_task_dependency`, and `wcs_domain_event` with named constraints and no `default02-default08` columns. - [ ] **Step 4: Implement mapping and optimistic updates** `TransportOrderMapper.updateState` must include `WHERE order_id = #{orderId} AND state_version = #{expectedVersion}` and increment `state_version`; repository throws `OptimisticLockingFailureException` when update count is 0. - [ ] **Step 5: Run mapping and schema text tests GREEN** Run: `mvn -pl ruoyi-wcs -am -Dtest=TransportOrderRowMapperTest test` Run the read-only verification script against the isolated WCS test database and expect one row per required table and unique constraint. - [ ] **Step 6: Commit** ```bash git add sql/wcs ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task ruoyi-wcs/src/main/resources/mapper/wcs/task ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task git commit -m "feat(wcs): persist transport orders and workflows" ``` ### Task 7: Wire the core module into RuoYi without changing legacy behavior **Files:** - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/WcsCoreConfiguration.java` - Create: `ruoyi-admin/src/test/java/com/ruoyi/wcs/WcsCoreWiringTest.java` - Create: `ruoyi-admin/src/test/java/com/ruoyi/wcs/TestPersistenceConfiguration.java` - Create: `ruoyi-admin/src/test/java/com/ruoyi/wcs/AdminWcsBoundaryArchitectureTest.java` - Verify: `ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java` - Verify: `ruoyi-admin/src/main/resources/application.yml` **Interfaces:** - Produces: Spring beans `TransportOrderUseCase`, `TransportOrderRepository`, `WcsClock`, and `IdGenerator`. - Does not change: `ScheduleThread`, `BaseSRgvThread`, `Wcs2WmsController`, or PLC writes. - Enforces: project/Admin packages may use `com.ruoyi.wcs.port.in..`, `com.ruoyi.wcs.port.out..`, and `com.ruoyi.wcs.query..`, but may not depend on `com.ruoyi.wcs.infrastructure.persistence..` or concrete Repository implementations. - [ ] **Step 1: Write a failing Spring wiring test** ```java @Test void exposesTheTransportOrderUseCaseFromTheWcsModule() { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { context.register(WcsCoreConfiguration.class, TestPersistenceConfiguration.class); context.refresh(); assertNotNull(context.getBean(TransportOrderUseCase.class)); } } ``` Create the test-only dependency configuration explicitly: ```java @TestConfiguration public class TestPersistenceConfiguration { @Bean public TransportOrderRepository transportOrderRepository() { return Mockito.mock(TransportOrderRepository.class); } @Bean public WorkflowDefinitionProvider workflowDefinitionProvider() { return Mockito.mock(WorkflowDefinitionProvider.class); } @Bean public DomainEventPublisher domainEventPublisher() { return Mockito.mock(DomainEventPublisher.class); } } ``` - [ ] **Step 2: Run RED** Run: `mvn -pl ruoyi-admin -am -Dtest=WcsCoreWiringTest test` - [ ] **Step 3: Add explicit core configuration** ```java @Configuration @ComponentScan(basePackages = "com.ruoyi.wcs") public class WcsCoreConfiguration { } ``` Do not alter `@MapperScan("com.ruoyi.**.mapper")`; add a test assertion that `TransportOrderMapper` is found under the existing wildcard scan. Add an ArchUnit rule importing `com.ruoyi.project..` and `com.ruoyi.api..` that fails on dependencies to `com.ruoyi.wcs.infrastructure.persistence..`; allow only the public ports, application use cases, domain value types required by those ports, and query DTOs. - [ ] **Step 4: Run the full foundation verification** Run: `mvn -pl ruoyi-wcs,ruoyi-admin -am test` Run: `git diff --check` Expected: tests pass, no PLC or scheduler source file is modified, and the module boundary test passes. - [ ] **Step 5: Commit** ```bash git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/WcsCoreConfiguration.java ruoyi-admin/src/test/java/com/ruoyi/wcs git commit -m "feat(wcs): wire core module into RuoYi" ```