import fs from 'node:fs' import { spawnSync } from 'node:child_process' const source = JSON.parse(fs.readFileSync('contracts/fields/kbx.fields.json', 'utf8')) const errors = [] const seen = new Set() const aliasOwners = new Map() const types = new Set(['text','code','integer','decimal','quantity','money','date','datetime','boolean','lookup','status']) if (!/^\d+\.\d+\.\d+$/.test(source.version ?? '')) errors.push('field dictionary version must be semver') for (const field of source.fields ?? []) { if (!/^[a-z][A-Za-z0-9]*$/.test(field.key ?? '')) errors.push(`invalid field key: ${field.key}`) if (seen.has(field.key)) errors.push(`duplicate field key: ${field.key}`) seen.add(field.key) if (!types.has(field.dataType)) errors.push(`invalid data type: ${field.key} ${field.dataType}`) if ((field.dataType === 'money' || field.dataType === 'quantity' || field.dataType === 'decimal') && (field.precision == null || field.scale == null)) errors.push(`${field.key}: numeric field requires precision and scale`) if (field.dataType === 'lookup' && !field.lookupEntity) errors.push(`${field.key}: lookup field requires lookupEntity`) if (field.readonly && field.importable) errors.push(`${field.key}: readonly field cannot be importable`) if (field.sensitive && !field.masking) errors.push(`${field.key}: sensitive field requires masking metadata`) if (field.deprecated && !field.replacementKey) errors.push(`${field.key}: deprecated field requires replacementKey`) for (const alias of [field.key, ...(field.aliases ?? [])]) { const normalized = alias.trim().toLocaleLowerCase('ko-KR') if (!normalized) continue const owner = aliasOwners.get(normalized) // Collision is only dangerous when both fields can participate in generic import mapping. if (owner && owner !== field.key) { const other = source.fields.find(x => x.key === owner) if (field.importable && other?.importable) errors.push(`import alias collision: "${alias}" => ${owner}, ${field.key}`) } else aliasOwners.set(normalized, field.key) } } const result = spawnSync(process.execPath, ['scripts/generate-field-contracts.mjs'], { stdio: 'inherit' }) if (result.status !== 0) process.exit(result.status ?? 1) const manifest = JSON.parse(fs.readFileSync('generated/field-manifest.json', 'utf8')) if (manifest.count !== source.fields.length) errors.push('generated field manifest count drift') const ts = fs.readFileSync('packages/kbx-contracts/src/generated/fieldCatalog.ts', 'utf8') const cs = fs.readFileSync('backend/Shared/Contracts/Generated/KbxFieldCatalog.g.cs', 'utf8') for (const field of source.fields) { if (!ts.includes(JSON.stringify(field.key))) errors.push(`TS field missing: ${field.key}`) if (!cs.includes(`"${field.key}"`)) errors.push(`C# field missing: ${field.key}`) } const orderImportTs = fs.readFileSync('apps/web/src/modules/oms/orders/import/order-import.definition.ts', 'utf8') const orderImportCs = fs.readFileSync('backend/Modules/OMS/Orders/Import/OrderImportDefinition.cs', 'utf8') if (!orderImportTs.includes("kbxImportField('orderQty')")) errors.push('OMS order import must consume canonical orderQty metadata on frontend') if (!orderImportCs.includes('I(KbxFieldKeys.OrderQty)')) errors.push('OMS order import must consume canonical orderQty metadata on backend') if (/\bfield:\s*['"]quantity['"]/.test(fs.readFileSync('apps/web/src/modules/oms/orders/register/order-register.definition.ts','utf8'))) errors.push('OMS order register still uses deprecated quantity field key') if (!fs.existsSync('backend/Shared/Contracts/KbxFieldOpenApiSchemaFilter.cs')) errors.push('OpenAPI field contract filter is missing') if (errors.length) { console.error('field governance FAIL') for (const e of errors) console.error(`- ${e}`) process.exit(1) } const adoption = spawnSync(process.execPath, ['scripts/report-field-adoption.mjs'], { stdio: 'inherit' }) if (adoption.status !== 0) process.exit(adoption.status ?? 1) console.log(`field governance PASS: ${source.fields.length} canonical fields`)