fix(bom): preserve resolved primary material

This commit is contained in:
WuYongJie 2026-08-13 12:36:47 +08:00
parent cdb807dcd5
commit 62017377ad
6 changed files with 34 additions and 12 deletions

View File

@ -218,7 +218,7 @@ const supportedV3AnalysisResult = {
schemaVersion: 'competitor-analysis/v3' as const,
structureBlueprint: {
schemaVersion: 'structure-blueprint/v2' as const,
knowledgeVersion: 'structure-priors/2026-08-13-v8',
knowledgeVersion: 'structure-priors/2026-08-13-v9',
supportStatus: 'SUPPORTED' as const,
categorySkill: {
selectedSkillId: 'TABLE',

View File

@ -276,7 +276,7 @@ async function runStructurePipeline(orderedFixture: readonly ProductionWardrobe2
}
const checkpoint = readBomCostingStructureCheckpoint({
kind: 'BOM_COSTING_STRUCTURE_CHECKPOINT',
version: 12,
version: 13,
sourceSkuIds: snapshot.skus.map((sku) => sku.externalSkuId),
blueprint,
completedGroupKeys: [],

View File

@ -222,7 +222,7 @@ describe('BOM v7 结构蓝图与 SKU 展开', () => {
const group = blueprint.structureGroups[0]!
const checkpointValue = {
kind: 'BOM_COSTING_STRUCTURE_CHECKPOINT',
version: 12,
version: 13,
sourceSkuIds: ['sku-1'],
blueprint,
completedGroupKeys: [group.structureGroupKey],
@ -307,7 +307,7 @@ describe('BOM v7 结构蓝图与 SKU 展开', () => {
)
const checkpoint = readBomCostingStructureCheckpoint({
kind: 'BOM_COSTING_STRUCTURE_CHECKPOINT',
version: 12,
version: 13,
sourceSkuIds: snapshot.skus.map((sku) => sku.externalSkuId),
blueprint,
completedGroupKeys: blueprint.structureGroups.map((group) => group.structureGroupKey),

View File

@ -140,9 +140,9 @@ const instantiationPayloadSchema = z.object({
const BOM_COSTING_STRUCTURE_CHECKPOINT_KIND =
'BOM_COSTING_STRUCTURE_CHECKPOINT' as const
// Version 12 invalidates instantiated lines created before trusted slot
// dimensions were preserved and axis-labelled evidence was parsed safely.
const BOM_COSTING_STRUCTURE_CHECKPOINT_VERSION = 12 as const
// Version 13 invalidates instantiated lines created before unrecognized
// component material prose stopped shadowing a resolved primary cost family.
const BOM_COSTING_STRUCTURE_CHECKPOINT_VERSION = 13 as const
const bomCostingStructureCheckpointSchema = z.object({
kind: z.literal(BOM_COSTING_STRUCTURE_CHECKPOINT_KIND),
@ -954,6 +954,16 @@ function materialDescriptorFromRecords(records: readonly MaterialFactRecord[]) {
)))
}
function materialDescriptorLayer(records: readonly MaterialFactRecord[]) {
const descriptors = records.flatMap((record) => (
materialDescriptorsFromText(materialFactText(record))
))
return {
hasRecognizedDescriptor: descriptors.length > 0,
descriptor: selectSingleMaterialDescriptor(descriptors),
}
}
function explicitPrimaryMaterialDescriptor(input: {
materialRecords: readonly MaterialFactRecord[]
skuId: string
@ -987,14 +997,16 @@ function resolvePhysicalMaterialDescriptor(input: {
))
// A conflict at a higher-priority scope remains unknown; a broader fact or
// model guess must not silently choose one side of that conflict.
if (skuSlotRecords.length > 0) return materialDescriptorFromRecords(skuSlotRecords)
const skuSlotMaterial = materialDescriptorLayer(skuSlotRecords)
if (skuSlotMaterial.hasRecognizedDescriptor) return skuSlotMaterial.descriptor
const productExactSlotRecords = slotRecords.filter((record) => (
!isBomCostingSkuSpecificFact(record, input.sku.externalSkuId) &&
materialFactNamesConcreteComponent(record)
))
if (productExactSlotRecords.length > 0) {
return materialDescriptorFromRecords(productExactSlotRecords)
const productExactSlotMaterial = materialDescriptorLayer(productExactSlotRecords)
if (productExactSlotMaterial.hasRecognizedDescriptor) {
return productExactSlotMaterial.descriptor
}
if (canInheritPrimaryMaterial(input.slot)) {
@ -1008,7 +1020,8 @@ function resolvePhysicalMaterialDescriptor(input: {
if (costFamilyDescriptor) return costFamilyDescriptor
}
if (slotRecords.length > 0) return materialDescriptorFromRecords(slotRecords)
const genericSlotMaterial = materialDescriptorLayer(slotRecords)
if (genericSlotMaterial.hasRecognizedDescriptor) return genericSlotMaterial.descriptor
if (!canInheritPrimaryMaterial(input.slot)) return undefined
return explicitPrimaryMaterialDescriptor({
materialRecords: allMaterialRecords,

View File

@ -26,7 +26,7 @@ import {
} from './category-skills'
export const BOM_COSTING_STRUCTURE_KNOWLEDGE_VERSION =
'structure-priors/2026-08-13-v8' as const
'structure-priors/2026-08-13-v9' as const
export type BomCostingStructureSlotPrior = BomCostingCategoryComponentDefinition

View File

@ -148,6 +148,15 @@ export function buildProductionMobileRattanStorageRack2FactSnapshot(): BomCostin
basis: 'SOURCE_TEXT',
confidence: 0.99,
},
{
factId: 'IMG_0039_fact_004',
kind: 'MATERIAL',
subject: '上层托盘及置物箱框体',
value: '表面呈棕色木纹外观',
relatedSkuIds: [],
basis: 'IMAGE',
confidence: 0.9,
},
{
factId: 'IMG_0039_fact_008',
kind: 'STRUCTURE',