For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Status: completed on 2026-07-03. P0 backend/runtime work and P1 frontend configuration/detail quantity panels are implemented and verified.
Goal: Build the platform-level transaction closure capabilities needed for fully low-code procurement and warehouse workflows without hardcoding procurement-specific business objects.
Architecture: Extend the existing low-code action engine with a FOREACH step that executes existing whitelisted child steps against collection items. Enhance record selectors to support multi-field search and context-driven filters, then expose quantity ledger read models through guarded query APIs that can be consumed by low-code detail panels.
Tech Stack: Java 17, Spring Boot 3.2, MyBatis-Plus XML mappers, Vue 3.5, Naive UI, Vite 7, pnpm.
ForeachActionStepExecutor.java: executes collection-based child steps using existing step executors.BusinessActionExecutionContext.java: stores scoped variables such as item and index.BusinessActionStepConfigHelper.java: resolves ${item.xxx} and context expressions consistently.BusinessActionExecutionService.java: exposes child-step execution safely to FOREACH or extracts a reusable step runner.BusinessActionDesigner.vue: adds a loop-step JSON template button.BusinessRecordSelectorService.java: supports multi-field search and expression-filter normalization.AiRecordSelectorModal.vue, AiFormItem.vue, ChildTableEditor.vue, record-selector-utils.js: resolves dynamic searchParams from runtime context before querying.BusinessQuantityQueryDTO.java and quantity query VO classes.BusinessQuantityQueryService.java and BusinessQuantityQueryController.java.business-app.js: add quantity query API wrappers.FOREACH, selector search/filtering, and quantity query service.Files:
forge-server/forge-framework/forge-plugin-parent/forge-plugin-generator/src/main/java/com/mdframe/forge/plugin/generator/service/businessapp/BusinessActionExecutionContext.javaModify: forge-server/forge-framework/forge-plugin-parent/forge-plugin-generator/src/main/java/com/mdframe/forge/plugin/generator/service/businessapp/BusinessActionStepConfigHelper.java
[x] Step 1: Add scoped variable storage
Add this field and helper methods to BusinessActionExecutionContext:
private Map<String, Object> scopedVariables = new LinkedHashMap<>();
public Object getScopedVariable(String key) {
return scopedVariables == null ? null : scopedVariables.get(key);
}
public Map<String, Object> getScopedVariables() {
return scopedVariables == null ? new LinkedHashMap<>() : scopedVariables;
}
public void setScopedVariables(Map<String, Object> scopedVariables) {
this.scopedVariables = scopedVariables == null ? new LinkedHashMap<>() : new LinkedHashMap<>(scopedVariables);
}
In BusinessActionStepConfigHelper.resolvePath, before form/record fallback, resolve the first path segment from context.getScopedVariables():
String firstSegment = field.contains(".") ? field.substring(0, field.indexOf('.')) : field;
if (context.getScopedVariables().containsKey(firstSegment)) {
Object scoped = context.getScopedVariables().get(firstSegment);
String rest = field.equals(firstSegment) ? "" : field.substring(firstSegment.length() + 1);
return StringUtils.isBlank(rest) ? scoped : readPathFromObject(scoped, rest);
}
Add readPathFromObject(Object source, String path) that supports Map<?, ?> values.
Run:
JAVA_HOME=/opt/homebrew/Cellar/openjdk@17/17.0.13/libexec/openjdk.jdk/Contents/Home PATH=/opt/homebrew/Cellar/openjdk@17/17.0.13/libexec/openjdk.jdk/Contents/Home/bin:$PATH mvn -pl forge-framework/forge-plugin-parent/forge-plugin-generator -am compile -DskipTests
Expected: compile succeeds.
Files:
forge-server/forge-framework/forge-plugin-parent/forge-plugin-generator/src/main/java/com/mdframe/forge/plugin/generator/service/businessapp/ForeachActionStepExecutor.javaBusinessActionExecutionService.javaTest: BusinessActionForeachStepExecutorTest.java
[x] Step 1: Extract step execution method
Expose a package-private method on BusinessActionExecutionService:
List<BusinessActionStepResultVO> executeNestedSteps(BusinessActionExecutionContext context, List<BusinessActionStepDTO> steps, int depth) {
if (depth > 2) {
throw new BusinessException("FOREACH 动作步骤最多嵌套 2 层");
}
return executeSteps(context, steps).stepResults();
}
ForeachActionStepExecutorCreate a component with supportType() returning FOREACH. It reads collectionPath, itemAlias, indexAlias, and steps from stepConfig; resolves the collection through BusinessActionStepConfigHelper.resolvePath; for every item it temporarily patches context.scopedVariables, calls actionExecutionService.executeNestedSteps(context, childSteps, depth + 1), and restores the old scoped variables in finally.
Create tests covering empty collection, two successful rows, variable mapping, and failure rollback behavior using mock child executors.
Run:
JAVA_HOME=/opt/homebrew/Cellar/openjdk@17/17.0.13/libexec/openjdk.jdk/Contents/Home PATH=/opt/homebrew/Cellar/openjdk@17/17.0.13/libexec/openjdk.jdk/Contents/Home/bin:$PATH mvn -pl forge-framework/forge-plugin-parent/forge-plugin-generator -Pdev,enable-tests -Dtest=BusinessActionForeachStepExecutorTest -Dsurefire.failIfNoSpecifiedTests=false test
Expected: tests run and pass.
Files:
BusinessRecordSelectorService.javaAiRecordSelectorModal.vueAiFormItem.vueChildTableEditor.vuerecord-selector-utils.jsTest: BusinessRecordSelectorServiceTest.java
[x] Step 1: Add selector filter normalization tests
Add tests for keyword fields code,name, internal field rejection, and dynamic search param resolution.
Change selector service so multi-field keywords are normalized into a structured search object consumed by dynamic CRUD or a selector-specific XML query. Continue rejecting system fields and _ fields.
Add resolveSelectorSearchParams(searchParams, context) to record-selector-utils.js. It should replace strings like ${formData.warehouseId}, ${record.id}, ${row.itemCode}, ${query.projectId} with values from the provided runtime context and omit null/empty values.
In AiFormItem.vue, pass { formData, record: formData, query: route.query }. In ChildTableEditor.vue, pass parent form data through existing context or a new prop if required.
Run backend selector tests and pnpm --dir forge-admin-ui build.
Files:
dto/businessapp and vo/businessapp.BusinessQuantityQueryService.javaBusinessQuantityQueryController.javabusiness-app.jsTest BusinessQuantityQueryServiceTest.java
[x] Step 1: Add DTO and VO
Create BusinessQuantityQueryDTO with accountCode, itemCode, dimensionKey, sourceObjectCode, sourceRecordId, sourceDetailId, operationType, and lockStatus.
Add explicit-field selectBalancePage, selectLedgerPage, and selectLockPage mapper queries with tenant and optional filters.
Expose POST /ai/business/quantity/query/balance, /ledger, and /lock with @SaCheckPermission("ai:businessQuantity:view").
Create a new Flyway script with NOT EXISTS inserts for ai:businessQuantity:view.
Run BusinessQuantityQueryServiceTest.
Files:
code-copilot/changes/lowcode-business-transaction-closure/execution-log.mdModify: tasks.md, test-spec.md, spec.md
[x] Step 1: Read automated testing standard
Run:
cat code-copilot/rules/automated-testing-standard.md
Use the commands in test-spec.md.
Append commands, results, warnings, and skipped items to execution-log.md. Update task status and spec execution table.
FOREACH, quantity action stays QUANTITY, permissions use ai:businessQuantity:view.