2026-09-08-export-pallet-data-identifiers.md 27 KB

export_pallet_data Identifier Update 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: Change export_pallet_data from inbound-order-number filtering to an explicit three-way choice of container numbers, bill-of-lading numbers, or overseas-warehouse inbound-time range, while preserving the 200-item limit, company isolation, async task contract, and pallet export output.

Architecture: Keep the Gateway as a schema/forwarding boundary and keep all business resolution in fmsoperate. Gateway accepts container_codes and bl_numbers as separate arrays and rejects ambiguous or mixed selectors; fmsoperate resolves each selector through its own indexed database branch before reusing the existing inbound-record → order → pallet-batch pipeline. The old inbound_numbers field is rejected at both the public schema and PHP task boundary.

Tech Stack: Python 3 standard-library MCP Gateway, ThinkPHP 6/PHP 7.1+, ThinkORM/MySQL, Redis queue, PHPUnit contract tests, Markdown project documentation.

Delivery boundary: Do not execute registration SQL, restart Gateway or workers, or commit Git changes. Preserve all unrelated user changes already present in the three repositories.


Files and responsibilities

Gateway (Y:/mcp)

  • Modify tools/export_pallet_data.py for metadata, runtime validation, and forwarding.
  • Modify app.py for CLI selector flags and payload construction.
  • Modify services/output_presenter.py to map new parameter errors to Chinese labels.
  • Modify tests/test_export_pallet_data_tool.py for the new closed schema, forwarding, limits, and CLI contract.
  • Modify tests/test_tool_description_boundaries.py so this number-based export requires explicit type clarification.
  • Modify tests/test_output_presenter.py for new field-label error mapping.
  • Modify README.md and CONTEXT.md to remove the old selector contract from Gateway-facing material.

Business backend (Y:/fmsoperate)

  • Modify app/mcp/validate/McpToolValidate.php for the new selector XOR rules and explicit legacy-field rejection.
  • Modify app/mcp/logic/McpPalletDataExportLogic.php for normalization, validation, task parameters, and model calls.
  • Modify app/mcp/model/McpPalletDataExportModel.php for separate container and bill-of-lading lookup branches.
  • Modify app/mcp/logic/McpPalletDataExportService.php for the new persisted task parameters.
  • Modify app/mcp/controller/McpToolsController.php so audit payload allowlisting records the new selector fields.
  • Modify app/job/think/handle/inside/McpPalletDataExport.php so queued-task validation accepts only the new selector contract.
  • Do not modify the route, queue action, task-reference source list, or export output generation.

Contract tests (Y:/settlement_tests)

  • Modify tests/Unit/FmsOperate/McpPalletDataExportTest.php for validator, logic, model-shape, identity, and new selector behavior.
  • Modify tests/Unit/FmsOperate/McpPalletDataExportJobTest.php for worker payload validation and legacy-task rejection.
  • Modify docs/manual/mcp-async-export-workbuddy-cases.md with new pallet selector placeholders and manual cases.

Central documentation (Y:/all_project_docs)

  • Modify fmsoperate/requirements.md, fmsoperate/tech-specs.md, fmsoperate/user-structure.md, and fmsoperate/timeline.md.
  • Modify mcp/overview.md, mcp/requirements.md, mcp/tech-specs.md, mcp/user-structure.md, and mcp/timeline.md.
  • Modify fmsoperate/sql/mcp_add_pallet_data_export_tool.sql description only; do not execute it.

Task 1: Add failing Gateway contract tests

Files:

  • Modify: Y:/mcp/tests/test_export_pallet_data_tool.py
  • Modify: Y:/mcp/tests/test_tool_description_boundaries.py
  • Modify: Y:/mcp/tests/test_output_presenter.py

  • [ ] Step 1: Replace the old schema expectations.

Change the schema test to require exactly:

{
    'container_codes',
    'bl_numbers',
    'inbound_time_start',
    'inbound_time_end',
}

Keep additionalProperties=False, assert both number fields have maxItems == 200, and assert that inbound_numbers, page, limit, ids, and inbound_id are absent.

  • Step 2: Add forwarding tests for both number types.

Add separate tests that call the tool with trimmed duplicate values and assert the exact payload:

ExportPalletDataTool(client).call(
    container_codes=[' CONT-1 ', 'CONT-1', 'CONT-2'],
)
assert client.calls[-1][2] == {
    'container_codes': ['CONT-1', 'CONT-2'],
}

ExportPalletDataTool(client).call(
    bl_numbers=[' BL-1 ', 'BL-1'],
)
assert client.calls[-1][2] == {
    'bl_numbers': ['BL-1'],
}

Retain the existing date-only and date-time boundary tests, changing only their surrounding selector assumptions.

  • Step 3: Add the new validation boundary cases.

Cover all of these cases:

tool.call(container_codes=['CONT-1'], bl_numbers=['BL-1'])  # raises ValueError
tool.call(container_codes=['CONT-1'], inbound_time_start='2026-09-01',
          inbound_time_end='2026-09-02')                    # raises ValueError
tool.call(bl_numbers=['BL-1'], inbound_time_start='2026-09-01',
          inbound_time_end='2026-09-02')                    # raises ValueError
tool.call(container_codes=[])                               # raises ValueError
tool.call(bl_numbers=['BL-' + 'x' * 100])                   # accepted at 100 chars
tool.call(container_codes=['x' * 101])                      # raises ValueError
tool.call(bl_numbers=['BL-{0}'.format(i) for i in range(201)])  # raises ValueError
tool.call()                                                 # raises ValueError

The metadata test must also prove that passing the old field is impossible through the closed schema; do not add inbound_numbers back to the Python call signature.

  • Step 4: Update description-boundary tests.

Include ExportPalletDataTool() in the number-type clarification group and require its description to contain:

  • 号码类型不明确时必须先询问用户
  • 不得根据号码格式猜测
  • 不得跨字段或跨工具试查

Keep the existing asynchronous export boundary assertions.

  • Step 5: Add Presenter field-label coverage.

Add a MCP_1401 case for container_codes and one for bl_numbers, asserting the public message is respectively 柜号参数不正确 and 提单号参数不正确, and that raw Python field names are absent from serialized output.

  • Step 6: Run the Gateway tests and confirm they fail for the old implementation.

Run from Y:/mcp:

python -m unittest tests.test_export_pallet_data_tool tests.test_tool_description_boundaries tests.test_output_presenter

Expected result: failures identify the still-existing inbound_numbers schema/signature/CLI behavior and missing field labels. Do not change production code until these tests demonstrate the contract gap.

Task 2: Implement the Gateway selector contract

Files:

  • Modify: Y:/mcp/tools/export_pallet_data.py
  • Modify: Y:/mcp/app.py
  • Modify: Y:/mcp/services/output_presenter.py

  • [ ] Step 1: Replace the metadata fields and descriptions.

Define container_codes and bl_numbers with the same array constraints as the former number field:

{
    'type': 'array',
    'minItems': 1,
    'maxItems': 200,
    'items': {
        'type': 'string',
        'minLength': 1,
        'maxLength': 100,
        'pattern': r'.*\S.*',
    },
}

Describe each field using its Chinese business label, say that it is only valid when the user explicitly identifies that type, and prohibit other number types and internal IDs. Update the tool description to say the selector is exactly “柜号 / 提单号 / 海外仓入库时间”三选一. Explicitly state that an unclear number type requires a user question before calling and that the tool must not guess or try another field.

  • Step 2: Implement a single normalized array helper.

Use one helper with the following behavior for both fields:

def _clean_numbers(self, field_name, values):
    if not isinstance(values, list) or not values:
        raise ValueError('{0} must be a non-empty list'.format(field_name))
    cleaned = []
    for value in values:
        if not isinstance(value, str):
            raise ValueError('{0} items must be strings'.format(field_name))
        item = value.strip()
        if not item or len(item) > 100:
            raise ValueError(
                '{0} items must be 1 to 100 chars'.format(field_name)
            )
        if item not in cleaned:
            cleaned.append(item)
    if len(cleaned) > 200:
        raise ValueError('at most 200 {0}'.format(field_name))
    return cleaned
  • Step 3: Change call() to enforce the three-way XOR.

The public signature must be:

def call(
    self,
    container_codes=None,
    bl_numbers=None,
    inbound_time_start=None,
    inbound_time_end=None,
    request_id='rq_export_pallet_data',
):

Treat a non-None number argument as supplied, even if it is an empty list, so malformed empty arrays cannot silently disappear. Reject both number arguments together, reject any number argument combined with a time range, require both time bounds when time is selected, and reject an entirely empty payload. Forward exactly one cleaned selector or the two original time-bound strings.

  • Step 4: Update the CLI branch without affecting other tools.

--container-codes and --bl-numbers already exist as shared CLI options for other tools. Remove only the obsolete --inbound-numbers parser option and change the export_pallet_data branch to build:

if args.container_codes:
    tool_args['container_codes'] = parse_string_list(args.container_codes)
if args.bl_numbers:
    tool_args['bl_numbers'] = parse_string_list(args.bl_numbers)
if args.inbound_time_start:
    tool_args['inbound_time_start'] = args.inbound_time_start
if args.inbound_time_end:
    tool_args['inbound_time_end'] = args.inbound_time_end
tool_args.pop('page', None)
tool_args.pop('limit', None)
  • Step 5: Add safe parameter labels.

Extend OutputPresenter.FIELD_LABELS['export_pallet_data'] with:

'container_codes': '柜号',
'bl_numbers': '提单号',
'inbound_time_start': '入库时间开始',
'inbound_time_end': '入库时间结束',

Do not expose raw field names in the public error text.

  • Step 6: Re-run the focused Gateway tests.

Run:

python -m unittest tests.test_export_pallet_data_tool tests.test_tool_description_boundaries tests.test_output_presenter

Expected result: all focused tests pass, including the unchanged 26-tool registry and 25-safe-tool assertions.

Task 3: Add failing fmsoperate contract tests

Files:

  • Modify: Y:/settlement_tests/tests/Unit/FmsOperate/McpPalletDataExportTest.php
  • Modify: Y:/settlement_tests/tests/Unit/FmsOperate/McpPalletDataExportJobTest.php

  • [ ] Step 1: Update Validate test inputs to the new selector names.

Change the accepted cases to:

$this->assertTrue($this->validator()->check([
    'container_codes' => ['CONT-1'],
]));
$this->assertTrue($this->validator()->check([
    'bl_numbers' => ['BL-1'],
]));
$this->assertTrue($this->validator()->check([
    'inbound_time_start' => '2026-09-01',
    'inbound_time_end' => '2026-09-07 18:30:00',
]));
  • Step 2: Add PHP boundary assertions.

Assert false for:

[
    'container_codes' => ['CONT-1'],
    'bl_numbers' => ['BL-1'],
]
[
    'container_codes' => ['CONT-1'],
    'inbound_time_start' => '2026-09-01',
    'inbound_time_end' => '2026-09-02',
]
[
    'inbound_numbers' => ['INB-1'],
]

Retain assertions for one-sided time, over-31-day time, 201 items, forbidden pagination, ids, and inbound_id.

  • Step 3: Update trusted-identity submission coverage.

Use container_codes in the submission test and assert the submitted task parameters contain the normalized new field, trusted company_id, operator_id, is_super, and mcp_source_tool, while still excluding ADMIN_ID.

Add a second submission assertion using bl_numbers so both code paths persist the correct selector.

  • Step 4: Add model query-shape guards.

Assert the model source contains fms_outbound, container_code, fms_booking_detail, and bl_number, keeps company_id, does not contain CONVERT_TZ or COUNT(, and does not contain a combined container_code|bl_number OR expression. This freezes the performance requirement that the two number types use separate branches.

  • Step 5: Update worker fixtures and add legacy-task rejection.

Change existing queued-task JSON fixtures from inbound_numbers to container_codes. Add a successful bl_numbers fixture. Add a test whose task JSON contains only inbound_numbers; assert the worker does not start the task, exporter is not called, and the task is marked failed.

  • Step 6: Run the focused PHP tests and confirm they fail before implementation.

Run from Y:/settlement_tests:

bin\test.bat fmsoperate --filter McpPalletDataExport

Expected result: failures identify the old validator field, old Logic parameter payload, missing model branches, and worker assumptions. Do not weaken the new assertions to accommodate the old contract.

Task 4: Implement fmsoperate validation and orchestration

Files:

  • Modify: Y:/fmsoperate/app/mcp/validate/McpToolValidate.php
  • Modify: Y:/fmsoperate/app/mcp/logic/McpPalletDataExportLogic.php
  • Modify: Y:/fmsoperate/app/mcp/controller/McpToolsController.php

  • [ ] Step 1: Collapse the duplicate exportPalletData scene definition.

Keep one scene definition containing only:

'container_codes',
'bl_numbers',
'inbound_time_start',
'inbound_time_end',
'page',
'limit',
'ids',
'inbound_id',
'inbound_numbers',

The last five legacy/forbidden keys must be attached to palletExportForbidden, with inbound_numbers included only to return MCP_1401 when an old caller sends it. Do not expose it in the Gateway schema or accept it in Logic normalization.

  • Step 2: Apply the same array and range rules to both new fields.

Keep the existing 1–200, string, non-empty, 100-character palletNumberArray behavior and attach it to both container_codes and bl_numbers. Update the XOR callback to evaluate:

$hasContainer = is_array($data['container_codes'] ?? null)
    && $data['container_codes'] !== [];
$hasBl = is_array($data['bl_numbers'] ?? null)
    && $data['bl_numbers'] !== [];
$hasStart = trim((string)($data['inbound_time_start'] ?? '')) !== '';
$hasEnd = trim((string)($data['inbound_time_end'] ?? '')) !== '';

return !($hasContainer && $hasBl)
    && !($hasContainer && ($hasStart || $hasEnd))
    && !($hasBl && ($hasStart || $hasEnd))
    && (($hasContainer || $hasBl) || ($hasStart && $hasEnd));

Retain the existing date parser and maximum 31-calendar-day rule.

  • Step 3: Normalize only the new fields in Logic.

Replace inbound_numbers normalization with two independent de-duplicating loops for container_codes and bl_numbers. The normalized result must always contain both arrays, even when one is empty:

[
    'container_codes' => $containerCodes,
    'bl_numbers' => $blNumbers,
    'inbound_time_start' => ...,
    'inbound_time_end' => ...,
    'request_id' => ...,
]

buildExportParams() must add only these normalized selectors plus trusted company_id, timezone, operator, operator_id, is_super, and mcp_source_tool; never copy caller-supplied identity fields.

  • Step 4: Update Logic validation and model calls.

validateFilters() must enforce the same three-way XOR as Validate, including per-field 200/100 limits. hasExportableData() and findPalletIds() calls must receive both arrays and both time bounds. Keep the existing permission checks, no-data MCP_1601, async submission, task reference, and retry response unchanged.

  • Step 5: Update audit allowlisting in the controller.

In exportPalletData(), replace the old log-field list with:

[
    'container_codes',
    'bl_numbers',
    'inbound_time_start',
    'inbound_time_end',
]

The route, route code, permission checks, response envelope, and task-source mapping remain unchanged.

  • Step 6: Run the focused PHP tests.

Run:

bin\test.bat fmsoperate --filter McpPalletDataExport

Expected result: Validate and Logic tests pass; model and worker tests remain red until Tasks 5 and 6 are complete.

Task 5: Implement separate model lookup branches and service persistence

Files:

  • Modify: Y:/fmsoperate/app/mcp/model/McpPalletDataExportModel.php
  • Modify: Y:/fmsoperate/app/mcp/logic/McpPalletDataExportService.php

  • [ ] Step 1: Change the model method signatures.

Use this argument order consistently:

public function hasExportableData(
    $companyId,
    array $containerCodes,
    array $blNumbers,
    $timeStart,
    $timeEnd
)

public function findPalletIds(
    $companyId,
    array $containerCodes,
    array $blNumbers,
    $timeStart,
    $timeEnd
)
  • Step 2: Add the container-code branch.

Add findInboundIdsByContainerCodes($companyId, array $codes) using one query that:

Db::table('fms_destination_inbound')->alias('di')
    ->join(['fms_outbound' => 'o'], 'di.outbound_id = o.id')
    ->where('di.company_id', (int)$companyId)
    ->where('di.is_delete', 0)
    ->where('o.container_code', 'in', $codes)
    ->field('di.id')
    ->group('di.id')

Return integer inbound IDs. Do not loop over the 200 values.

  • Step 3: Add the bill-of-lading branch.

Add findInboundIdsByBlNumbers($companyId, array $numbers) using:

Db::table('fms_destination_inbound')->alias('di')
    ->join(['fms_outbound' => 'o'], 'di.outbound_id = o.id')
    ->join(['fms_booking_detail' => 'bd'], 'o.booking_detail_id = bd.id')
    ->where('di.company_id', (int)$companyId)
    ->where('di.is_delete', 0)
    ->where('bd.bl_number', 'in', $numbers)
    ->field('di.id')
    ->group('di.id')

Return integer inbound IDs and keep the branch separate from the container query. Do not use where('o.container_code|bd.bl_number', ...).

  • Step 4: Select exactly one branch in findPalletIds().

Use this branch order:

if ($containerCodes !== []) {
    $inboundIds = $this->findInboundIdsByContainerCodes($companyId, $containerCodes);
} elseif ($blNumbers !== []) {
    $inboundIds = $this->findInboundIdsByBlNumbers($companyId, $blNumbers);
} else {
    $inboundIds = $this->findInboundIdsByTime($companyId, $timeStart, $timeEnd);
}

Keep the current inbound ID → order ID → pallet ID stages, company condition on pallet batches, and no COUNT behavior.

  • Step 5: Pass new selectors from the async Service.

Replace the old Service call with:

$palletIds = $this->model->findPalletIds(
    $companyId,
    is_array($params['container_codes'] ?? null)
        ? $params['container_codes'] : [],
    is_array($params['bl_numbers'] ?? null)
        ? $params['bl_numbers'] : [],
    (string)($params['inbound_time_start'] ?? ''),
    (string)($params['inbound_time_end'] ?? '')
);

Do not change Excel headers, row grouping, attachment download behavior, zip layout, uploader behavior, or temporary-file cleanup.

  • Step 6: Run PHP tests and syntax checks.

Run:

bin\test.bat fmsoperate --filter McpPalletDataExport
php -l Y:\fmsoperate\app\mcp\validate\McpToolValidate.php
php -l Y:\fmsoperate\app\mcp\logic\McpPalletDataExportLogic.php
php -l Y:\fmsoperate\app\mcp\model\McpPalletDataExportModel.php
php -l Y:\fmsoperate\app\mcp\logic\McpPalletDataExportService.php
php -l Y:\fmsoperate\app\mcp\controller\McpToolsController.php

Expected result: focused tests pass and every changed PHP file reports no syntax errors.

Task 6: Tighten queued-task validation and consumer regression tests

Files:

  • Modify: Y:/fmsoperate/app/job/think/handle/inside/McpPalletDataExport.php
  • Modify: Y:/settlement_tests/tests/Unit/FmsOperate/McpPalletDataExportJobTest.php

  • [ ] Step 1: Validate the persisted selector contract before starting.

In validatedParams(), reject any task containing inbound_numbers. Accept exactly one of:

$containerCodes = $params['container_codes'] ?? [];
$blNumbers = $params['bl_numbers'] ?? [];
$hasContainer = is_array($containerCodes) && $containerCodes !== [];
$hasBl = is_array($blNumbers) && $blNumbers !== [];
$hasTime = trim((string)($params['inbound_time_start'] ?? '')) !== ''
    && trim((string)($params['inbound_time_end'] ?? '')) !== '';

if (
    ($hasContainer && $hasBl)
    || (($hasContainer || $hasBl) && $hasTime)
    || (!$hasContainer && !$hasBl && !$hasTime)
) {
    return false;
}

Also require each new array to contain strings with 1–100-character trimmed values and at most 200 entries, so a manually altered task cannot bypass the HTTP validator.

  • Step 2: Preserve worker identity and terminal-state behavior.

Do not change message/company/operator matching, atomic queued-task start, exporter invocation, completed file mapping, failed-state update, or exception rethrow. Only the persisted selector validation changes.

  • Step 3: Update and extend job tests.

Keep the existing success, identity mismatch, and exception tests, replacing old task JSON with container_codes. Add a bill-of-lading success fixture and a legacy inbound_numbers fixture that never reaches the exporter and ends in status 3.

  • Step 4: Run the job-focused test.

Run:

bin\test.bat fmsoperate --filter McpPalletDataExportJob

Expected result: all worker identity, success, failure, and legacy-selector tests pass.

Task 7: Update current documentation and SQL registration description

Files:

  • Modify: Y:/mcp/README.md
  • Modify: Y:/mcp/CONTEXT.md
  • Modify: Y:/all_project_docs/fmsoperate/requirements.md
  • Modify: Y:/all_project_docs/fmsoperate/tech-specs.md
  • Modify: Y:/all_project_docs/fmsoperate/user-structure.md
  • Modify: Y:/all_project_docs/fmsoperate/timeline.md
  • Modify: Y:/all_project_docs/fmsoperate/sql/mcp_add_pallet_data_export_tool.sql
  • Modify: Y:/all_project_docs/mcp/overview.md
  • Modify: Y:/all_project_docs/mcp/requirements.md
  • Modify: Y:/all_project_docs/mcp/tech-specs.md
  • Modify: Y:/all_project_docs/mcp/user-structure.md
  • Modify: Y:/all_project_docs/mcp/timeline.md
  • Modify: Y:/settlement_tests/docs/manual/mcp-async-export-workbuddy-cases.md

  • [ ] Step 1: Replace the fmsoperate business contract.

In the export_pallet_data sections, state that the selector is exactly one of:

container_codes(柜号数组,1–200)
bl_numbers(提单号数组,1–200)
inbound_time_start + inbound_time_end(最多31个日历日)

State that each number is at most 100 characters, container and bill-of-lading arrays cannot coexist, neither can mix with time, and inbound_numbers/internal IDs/order numbers/pallet-batch numbers are unsupported. Preserve the existing company isolation, permissions, async task, output, attachment, and MCP_1601 behavior.

  • Step 2: Replace Gateway user-facing guidance.

Update the Gateway README and central MCP documents to tell the caller:

  1. Ask whether a supplied number is a cabinet/container number or a bill-of-lading number when the user has not said.
  2. Do not infer from formatting or try both fields.
  3. Use only one number type with up to 200 values, or use the time range.
  4. After submission, query the task separately with query_export_task.

Remove statements saying the tool accepts an inbound number.

  • Step 3: Update current context and acceptance manual.

Replace stale terminology in Y:/mcp/CONTEXT.md. Add pallet export placeholders for <有权柜号> and <有权提单号> to the manual, then add cases for:

  • successful container-number submission;
  • successful bill-of-lading submission;
  • ambiguous number requiring clarification;
  • container and bill-of-lading arrays mixed and rejected;
  • 201 values rejected;
  • number mixed with time rejected;
  • async task follow-up unchanged.

Do not add real credentials, URLs, task references, or customer data.

  • Step 4: Update the SQL registration description only.

Change the st_mcp_tool_registry.description text in mcp_add_pallet_data_export_tool.sql from inbound-number/time wording to container-number/bill-of-lading/time wording. Keep status defaults and the idempotent ON DUPLICATE KEY UPDATE behavior unchanged. Do not run the SQL or change the execution status in sql/README.md.

  • Step 5: Check document consistency.

Search all affected documentation for the old active contract:

rg -n "入仓单号数组|入仓单号或海外仓入库时间|inbound_numbers" Y:\mcp Y:\all_project_docs\fmsoperate Y:\all_project_docs\mcp Y:\settlement_tests\docs

Remaining matches must be limited to the design/implementation history explicitly saying that the old field is removed or rejected; no current usage instructions may tell callers to send it.

Task 8: Full verification and handoff

Files: all changed files from Tasks 1–7.

  • Step 1: Run all Gateway unit tests.

From Y:/mcp:

python -m unittest discover -s tests -p "test_*.py"
python -m coverage run -m unittest discover -s tests -p "test_*.py"
python -m coverage report -m --fail-under=100

Expected result: all tests pass and the strict coverage threshold is satisfied. Do not claim coverage success without the actual report.

  • Step 2: Run fmsoperate contract tests.

From Y:/settlement_tests:

bin\test.bat fmsoperate --filter McpPalletDataExport

Then run the full fmsoperate suite:

bin\test.bat fmsoperate

Expected result: both the focused pallet-export tests and the full fmsoperate test configuration pass.

  • Step 3: Run PHP syntax checks for every changed PHP file.

Run php -l for:

Y:/fmsoperate/app/mcp/validate/McpToolValidate.php
Y:/fmsoperate/app/mcp/logic/McpPalletDataExportLogic.php
Y:/fmsoperate/app/mcp/model/McpPalletDataExportModel.php
Y:/fmsoperate/app/mcp/logic/McpPalletDataExportService.php
Y:/fmsoperate/app/mcp/controller/McpToolsController.php
Y:/fmsoperate/app/job/think/handle/inside/McpPalletDataExport.php
  • Step 4: Run diff and status checks without altering unrelated work.

Run from each repository:

git diff --check
git status --short

Run the same checks from Y:/all_project_docs. Review that only the requested files and the approved design/plan are changed; do not reset, checkout, clean, commit, execute SQL, or restart services.

  • Step 5: Review the final contract manually.

Confirm all of the following from code and tests:

  • Gateway metadata and PHP Validate both reject the old inbound-number selector.
  • Container and bill-of-lading selectors are separate and mutually exclusive.
  • Time remains a third mutually exclusive selector.
  • 200 values are handled in one batch without per-number queries.
  • Container lookup uses fms_outbound.container_code.
  • Bill-of-lading lookup uses fms_booking_detail.bl_number.
  • Company isolation, permission checks, task references, async queue, 15-column output, attachment behavior, and safe task status responses are unchanged.