fix(selfop): persist bounded next-root quantity diagnostics

This commit is contained in:
2026-10-14 04:59:05 +08:00
parent 765b6e6208
commit 913b9e9262
17 changed files with 1142 additions and 914 deletions
@@ -1,40 +1,168 @@
openapi: 3.0.3
info:
title: Internal bounded self-operated quantity diagnostics
version: 1.0.0
title: Internal next-root bounded quantity diagnostics
version: 2.0.0
description: Explicit arm only; no automatic rearm. Bind next normal automatic historical OUTBOUND_QUERY root
within 6h. Sampling expires 4h after first LIST observedAt, never later than armedAt+10h. Lastleg session lock
is receiver acceptance. Source root creation may predate arm. LOCATE, manual/validation roots, current excluded
root and different bound root are rejected/ignored. At most 100 distinct LIST candidates total, 2000 retained
SKU rows across both sides, 2MiB canonical evidence plus promotion overhead. Pending-to-final never refunds
quota. First unknown or SKU mismatch persists INCONCLUSIVE and stops; any known inequality among list/forecast/actual
persists QUANTITY_DIFFERENCE and stops. Original side timestamps/attempts are immutable; exact replay returns
stored receipt even after termination, changed same-side evidence is rejected. Root termination drains only
accepted candidates via an explicit Integration FIFO boundary without extending expiry. Fewer than 100 complete
pairs is ROOT_ENDED_INSUFFICIENT; unpaired/lost tail is INCOMPLETE. Producer restart retains persisted candidates
but marks delivery uncertain. Every request/response is stream-bounded to 262144 bytes. Never exposed through
Gateway, never creates supplier requests or changes business facts.
paths:
/internal/lastleg/self-operated-analytics/quantity-diagnostics/orders:
/internal/lastleg/self-operated-analytics/quantity-diagnostics/arm:
post:
operationId: receiveSelfopQuantityDiagnosticOrder
description: >-
Internal service-token only; never routed through Gateway. One complete paired order per request.
Quantities are evidence only. Session scope and UTC time interval are frozen by server configuration.
Maximum 100 orders, 2000 SKU rows and 2097152 canonical UTF8 bytes per session;
200 SKU rows and 262144 wire/canonical bytes per request; at most 30 minutes.
First known forecast/actual difference closes capture regardless of comparison classification.
Same identity and canonical evidence is idempotent, including after closure; changed evidence is 409.
operationId: quantityDiagnosticArm
security:
- internalServiceToken: []
- internalServiceToken: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PairedOrder'
$ref: '#/components/schemas/Arm'
responses:
'200':
description: Diagnostic receipt in the standard ApiResponse wrapper; code is string 200.
description: ApiResponse code 200. status data is null if no grant exists.
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
code: { type: string, example: '200' }
data: { $ref: '#/components/schemas/Receipt' }
'400': { description: 'Invalid JSON, unknown fields, invalid scope/evidence, duplicate SKU or request limit.' }
'401': { description: Invalid internal service token. }
'409': { description: Session configuration changed or order identity reused with different evidence. }
'503': { description: Internal service token unconfigured or diagnostic storage unavailable. }
code:
type: string
message:
type: string
nullable: true
data:
$ref: '#/components/schemas/View'
nullable: true
required:
- code
- data
'400':
description: Invalid scope, changed frozen evidence, invalid observation or disabled feature.
'401':
description: Missing or invalid internal service token.
'503':
description: Diagnostic admission, dedicated connection or storage unavailable; collection is unaffected.
/internal/lastleg/self-operated-analytics/quantity-diagnostics/status:
post:
operationId: quantityDiagnosticStatus
security:
- internalServiceToken: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
responses:
'200':
description: ApiResponse code 200. status data is null if no grant exists.
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
code:
type: string
message:
type: string
nullable: true
data:
$ref: '#/components/schemas/View'
nullable: true
required:
- code
- data
'400':
description: Invalid scope, changed frozen evidence, invalid observation or disabled feature.
'401':
description: Missing or invalid internal service token.
'503':
description: Diagnostic admission, dedicated connection or storage unavailable; collection is unaffected.
/internal/lastleg/self-operated-analytics/quantity-diagnostics/observe:
post:
operationId: quantityDiagnosticObserve
security:
- internalServiceToken: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Event'
responses:
'200':
description: ApiResponse code 200. status data is null if no grant exists.
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
code:
type: string
message:
type: string
nullable: true
data:
$ref: '#/components/schemas/Receipt'
nullable: true
required:
- code
- data
'400':
description: Invalid scope, changed frozen evidence, invalid observation or disabled feature.
'401':
description: Missing or invalid internal service token.
'503':
description: Diagnostic admission, dedicated connection or storage unavailable; collection is unaffected.
/internal/lastleg/self-operated-analytics/quantity-diagnostics/stop:
post:
operationId: quantityDiagnosticStop
security:
- internalServiceToken: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Stop'
responses:
'200':
description: ApiResponse code 200. status data is null if no grant exists.
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
code:
type: string
message:
type: string
nullable: true
data:
$ref: '#/components/schemas/View'
nullable: true
required:
- code
- data
'400':
description: Invalid scope, changed frozen evidence, invalid observation or disabled feature.
'401':
description: Missing or invalid internal service token.
'503':
description: Diagnostic admission, dedicated connection or storage unavailable; collection is unaffected.
components:
securitySchemes:
internalServiceToken:
@@ -42,64 +170,365 @@ components:
in: header
name: X-Internal-Service-Token
schemas:
PairedOrder:
Arm:
type: object
additionalProperties: false
required: [diagnosticSessionId, collectionSessionId, rootTaskId, sourceAccount, warehouseCode, ownerCode, orderNo, listObservation, detailObservation, skus]
properties:
diagnosticSessionId: { type: string, maxLength: 128 }
collectionSessionId: { type: string, maxLength: 128 }
rootTaskId: { type: string, maxLength: 128 }
sourceAccount: { type: string, maxLength: 64 }
warehouseCode: { type: string, maxLength: 64 }
ownerCode: { type: string, maxLength: 128 }
orderNo: { type: string, maxLength: 128 }
listObservation: { $ref: '#/components/schemas/Observation' }
detailObservation: { $ref: '#/components/schemas/Observation' }
skus:
diagnosticSessionId:
type: string
maxLength: 128
minLength: 1
sourceAccount:
type: string
maxLength: 64
minLength: 1
warehouseCodes:
type: array
minItems: 1
maxItems: 200
description: Union of list/detail SKUs; duplicate source lines aggregated, unknown never zero-filled.
items: { $ref: '#/components/schemas/Sku' }
maxItems: 8
uniqueItems: true
items:
type: string
maxLength: 64
minLength: 1
ownerCode:
type: string
maxLength: 128
minLength: 1
description: Explicit owner code, or * for all allowed owners.
excludedRootTaskId:
type: string
maxLength: 128
minLength: 1
nullable: true
description: Must match the actual started, unfinished automatic root at arm; null only when none exists.
required:
- diagnosticSessionId
- sourceAccount
- warehouseCodes
- ownerCode
Status:
type: object
additionalProperties: false
properties:
sourceAccount:
type: string
maxLength: 64
minLength: 1
producerId:
type: string
maxLength: 128
minLength: 1
attach:
type: boolean
description: True only on Integration process attachment; replacement marks delivery uncertain and fences
prior producer.
drained:
type: boolean
description: Only after observing DRAINING, closing intake and draining previously queued callbacks.
required:
- sourceAccount
- producerId
- attach
- drained
Stop:
type: object
additionalProperties: false
properties:
diagnosticSessionId:
type: string
maxLength: 128
minLength: 1
sourceAccount:
type: string
maxLength: 64
minLength: 1
producerId:
type: string
maxLength: 128
minLength: 1
reason:
type: string
enum:
- INCOMPLETE
- QUEUE_FULL
- OBSERVER_FAILED
- SHUTDOWN
required:
- diagnosticSessionId
- sourceAccount
- producerId
- reason
Observation:
type: object
additionalProperties: false
required: [observedAt, attemptId, skuSetComplete]
properties:
status: { type: string, nullable: true, maxLength: 32 }
status:
type: string
maxLength: 32
minLength: 1
nullable: true
outboundTime:
type: string
maxLength: 64
minLength: 1
nullable: true
maxLength: 32
description: Original Asia/Shanghai source text, null if absent; not reinterpreted as another timezone.
observedAt: { type: string, format: date-time }
attemptId: { type: string, maxLength: 128 }
skuSetComplete: { type: boolean }
observedAt:
type: string
format: date-time
attemptId:
type: string
maxLength: 128
minLength: 1
skuSetComplete:
type: boolean
required:
- status
- outboundTime
- observedAt
- attemptId
- skuSetComplete
Sku:
type: object
additionalProperties: false
required: [sku, listPresent, detailPresent]
properties:
sku: { type: string, maxLength: 128 }
listQty: { type: number, nullable: true, minimum: 0, multipleOf: 0.000001 }
forecastQty: { type: number, nullable: true, minimum: 0, multipleOf: 0.000001 }
actualQty: { type: number, nullable: true, minimum: 0, multipleOf: 0.000001 }
listPresent: { type: boolean }
detailPresent: { type: boolean }
Receipt:
type: object
required: [outcome, acceptedOrders, acceptedSkuRows, acceptedBytes]
properties:
outcome: { type: string, enum: [ACCEPTED, DUPLICATE, STOPPED] }
comparison:
sku:
type: string
maxLength: 128
minLength: 1
listQty:
type: number
nullable: true
enum: [SKU_SET_MISMATCH, QUANTITY_UNKNOWN, FORECAST_ACTUAL_DIFFERENCE, ALL_EQUAL, LIST_QUANTITY_DIFFERENCE]
minimum: 0
description: Up to 32 integer digits, at most 6 decimal places; unknown stays null.
forecastQty:
type: number
nullable: true
minimum: 0
description: Up to 32 integer digits, at most 6 decimal places; unknown stays null.
actualQty:
type: number
nullable: true
minimum: 0
description: Up to 32 integer digits, at most 6 decimal places; unknown stays null.
required:
- sku
- listQty
- forecastQty
- actualQty
Key:
type: object
additionalProperties: false
properties:
warehouseCode:
type: string
maxLength: 64
minLength: 1
ownerCode:
type: string
maxLength: 128
minLength: 1
orderNo:
type: string
maxLength: 128
minLength: 1
paired:
type: boolean
required:
- warehouseCode
- ownerCode
- orderNo
- paired
Event:
type: object
additionalProperties: false
properties:
diagnosticSessionId:
type: string
maxLength: 128
minLength: 1
sourceAccount:
type: string
maxLength: 64
minLength: 1
producerId:
type: string
maxLength: 128
minLength: 1
rootTaskId:
type: string
maxLength: 128
minLength: 1
collectionSessionId:
type: string
maxLength: 128
minLength: 1
operation:
type: string
enum:
- OUTBOUND_QUERY
- OUTBOUND_DETAIL
warehouseCode:
type: string
maxLength: 64
minLength: 1
ownerCode:
type: string
maxLength: 128
minLength: 1
orderNo:
type: string
maxLength: 128
minLength: 1
side:
type: string
enum:
- LIST
- DETAIL
observation:
$ref: '#/components/schemas/Observation'
skus:
type: array
maxItems: 200
items:
$ref: '#/components/schemas/Sku'
description: Empty allowed only when skuSetComplete=false; persists inconclusive evidence.
required:
- diagnosticSessionId
- sourceAccount
- producerId
- rootTaskId
- collectionSessionId
- operation
- warehouseCode
- ownerCode
- orderNo
- side
- observation
- skus
View:
type: object
additionalProperties: false
properties:
diagnosticSessionId:
type: string
maxLength: 128
minLength: 1
sourceAccount:
type: string
maxLength: 64
minLength: 1
rootTaskId:
type: string
maxLength: 128
minLength: 1
nullable: true
collectionSessionId:
type: string
maxLength: 128
minLength: 1
nullable: true
state:
type: string
enum:
- ARMED
- BOUND
- DRAINING
- TERMINAL
stopReason:
type: string
nullable: true
enum: [DISABLED, NOT_STARTED, EXPIRED, LIMIT_REACHED, FORECAST_ACTUAL_DIFFERENCE]
acceptedOrders: { type: integer }
acceptedSkuRows: { type: integer }
acceptedBytes: { type: integer, format: int64, description: Canonical whitelist JSON UTF8 bytes persisted in this session. }
startsAt:
type: string
format: date-time
nullable: true
expiresAt:
type: string
format: date-time
nullable: true
bindBefore:
type: string
format: date-time
nullable: true
warehouseCodes:
type: array
maxItems: 8
items:
type: string
maxLength: 64
minLength: 1
ownerCode:
type: string
maxLength: 128
minLength: 1
excludedRootTaskId:
type: string
maxLength: 128
minLength: 1
nullable: true
candidates:
type: integer
minimum: 0
maximum: 100
pairedOrders:
type: integer
minimum: 0
maximum: 100
retainedRows:
type: integer
minimum: 0
maximum: 2000
retainedBytes:
type: integer
minimum: 0
maximum: 2097152
deliveryUncertain:
type: boolean
selected:
type: array
maxItems: 100
items:
$ref: '#/components/schemas/Key'
required:
- diagnosticSessionId
- sourceAccount
- rootTaskId
- collectionSessionId
- state
- stopReason
- startsAt
- expiresAt
- bindBefore
- warehouseCodes
- ownerCode
- excludedRootTaskId
- candidates
- pairedOrders
- retainedRows
- retainedBytes
- deliveryUncertain
- selected
Receipt:
type: object
additionalProperties: false
properties:
outcome:
type: string
enum:
- ACCEPTED
- IGNORED
- DUPLICATE
- STOPPED
comparison:
type: string
nullable: true
enum:
- ALL_EQUAL
- QUANTITY_DIFFERENCE
- INCONCLUSIVE
session:
$ref: '#/components/schemas/View'
required:
- outcome
- comparison
- session