// Prepare a four-row additive data release. This program never connects to a database. // Apply and rollback SQL remain reviewable, separate deployment actions. import fs from 'node:fs/promises' import path from 'node:path' import {createHash,randomUUID} from 'node:crypto' import {fileURLToPath} from 'node:url' export const SOURCE_VERSION='enterprise-web-20261013.1' export const SOURCE_PACK_SHA256='1b77db4014c0f1cba9ae2b65c51d29c1eaf327bdb889151af935eb2f702085df' export const SOURCE_MIGRATION_SHA256='449c0bfaa1abedde3a244b7aa268dcf1a1d952483ebe4559cfbe02144dca3acf' export const SOURCE_FILES=Object.freeze({ 'references/brand-style-prompts.md':'b7b67dd0a2dbc66906869260d7746ee6276c1687d9ad6b45551691858c399e7a', 'references/gujia-visual-style.md':'2d90238d97b77e776a0efba0ec26278090ad8cecec23df6bb93451183b9de21a', }) const BRANDS=['顾家','欧意朗','索菲亚','亿家达'] const VISUAL=['palette','materials','props','lighting','composition','quality'] const sha=bytes=>createHash('sha256').update(bytes).digest('hex') const check=(value,message)=>{if(!value)throw Error(message)} const exactKeys=(value,keys)=>value&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).sort().join('|')===keys.slice().sort().join('|') const text=(value,max)=>typeof value==='string'&&value.trim().length>0&&value.length<=max export function validateBrand(profile){ check(exactKeys(profile,['name','tone','primaryColor','backgroundColor','textColor','fontFamily','status','visualRules','source']),'品牌字段与已验收契约不一致') check(BRANDS.includes(profile.name)&&text(profile.tone,2000),'仅允许四个已有来源的品牌') check(['primaryColor','backgroundColor','textColor'].every(k=>/^#[a-f0-9]{6}$/i.test(profile[k])),'品牌配色格式无效') check(profile.fontFamily==='Noto Sans SC'&&profile.status==='draft','不得改写原有字体和待确认状态') check(exactKeys(profile.visualRules,VISUAL)&&VISUAL.every(k=>text(profile.visualRules[k],2000)),'不能遗漏或补造原有visualRules') const source=profile.source check(exactKeys(source,['label','version','files','adaptation'])&&source.label==='企业知识库'&&source.version===SOURCE_VERSION&&text(source.adaptation,1000),'品牌来源或适配声明无效') check(Array.isArray(source.files)&&source.files.length>=1&&source.files.length<=2&&new Set(source.files.map(f=>f.name)).size===source.files.length,'品牌原文清单无效') for(const file of source.files)check(exactKeys(file,['name','sha256'])&&SOURCE_FILES[file.name]===file.sha256,'品牌原文哈希不符合已验收来源') return profile } const quote=s=>"'"+s.replaceAll("'","''")+"'" const literal=rows=>quote(JSON.stringify(rows))+'::jsonb' const desired=rows=>`SELECT * FROM jsonb_to_recordset(${literal(rows)}) AS d(id text,name text,revision integer,profile jsonb)` function prelude(readOnly){return `\\set ON_ERROR_STOP on\nSET client_encoding='UTF8';\nBEGIN ISOLATION LEVEL SERIALIZABLE${readOnly?' READ ONLY':''};\nSET LOCAL lock_timeout='5s';\nSET LOCAL statement_timeout='45s';\nSELECT set_config('jiansun.expected_database', :'expected_database', true);\nDO $identity$ BEGIN\n IF current_database() IS DISTINCT FROM current_setting('jiansun.expected_database') THEN RAISE EXCEPTION 'Database identity mismatch'; END IF;\n IF to_regclass('public.detail_generator_brand_profiles') IS NULL THEN RAISE EXCEPTION 'Existing brand profile table is missing; no schema will be created'; END IF;\nEND $identity$;\n`} function conflictGuard(rows){return `DO $brands$ DECLARE d record; BEGIN\n FOR d IN ${desired(rows)} LOOP\n IF EXISTS(SELECT 1 FROM public.detail_generator_brand_profiles p WHERE p.name=d.name OR p.id=d.id) THEN RAISE EXCEPTION 'Brand already exists; refusing overwrite: %',d.name; END IF;\n IF NOT EXISTS(SELECT 1 FROM public.categories c WHERE c.name=d.name) THEN RAISE EXCEPTION 'Existing category is missing: %',d.name; END IF;\n END LOOP;\nEND $brands$;\n`} export function sqlPackage(rows,packageSha){ const header=`-- Four existing knowledge profiles only. Package SHA256: ${packageSha}\n-- Supply psql -X -v expected_database= -f .\n-- No schema, user, material, project, design resource or audit table changes.\n` const dryRun=header+prelude(true)+conflictGuard(rows)+`SELECT json_build_object('mode','read_only_dry_run','wouldInsert',4,'existingProfiles',(SELECT count(*) FROM public.detail_generator_brand_profiles),'packageSha256',${quote(packageSha)});\nROLLBACK;\n` const apply=header+prelude(false)+`SELECT pg_advisory_xact_lock(20261022,130004);\nLOCK TABLE public.detail_generator_brand_profiles IN SHARE ROW EXCLUSIVE MODE;\n`+conflictGuard(rows)+`INSERT INTO public.detail_generator_brand_profiles(id,name,profile,revision)\nSELECT id,name,profile,revision FROM (${desired(rows)}) desired\nRETURNING id,name,revision,profile->'source'->>'version' AS source_version,xmin::text AS import_xmin;\nSELECT json_build_object('applied',4,'packageSha256',${quote(packageSha)},'importXmin',(SELECT min(xmin::text) FROM public.detail_generator_brand_profiles WHERE id IN (${rows.map(r=>quote(r.id)).join(',')})),'rows',(SELECT json_agg(json_build_object('id',id,'name',name,'revision',revision)) FROM public.detail_generator_brand_profiles WHERE id IN (${rows.map(r=>quote(r.id)).join(',')})));\nCOMMIT;\n` const verify=header+prelude(true)+`DO $verify$ DECLARE d record; BEGIN\n FOR d IN ${desired(rows)} LOOP\n IF NOT EXISTS(SELECT 1 FROM public.detail_generator_brand_profiles p WHERE p.id=d.id AND p.name=d.name AND p.revision=1 AND p.profile=d.profile) THEN RAISE EXCEPTION 'Imported profile differs from exact source payload: %',d.name; END IF;\n END LOOP;\nEND $verify$;\nSELECT json_build_object('verified',4,'packageSha256',${quote(packageSha)},'importXmin',(SELECT min(xmin::text) FROM public.detail_generator_brand_profiles WHERE id IN (${rows.map(r=>quote(r.id)).join(',')})));\nROLLBACK;\n` const rollback=header+`-- Also supply -v import_xmin=.\n-- Abort on any changed row or referenced ID; never modify old project snapshots.\n`+prelude(false)+`SELECT set_config('jiansun.import_xmin', :'import_xmin', true);\nSELECT pg_advisory_xact_lock(20261022,130004);\nLOCK TABLE public.detail_generator_brand_profiles IN SHARE ROW EXCLUSIVE MODE;\nDO $rollback$\nDECLARE d record; r record; used boolean; tables text[]:=ARRAY['detail_generator_projects','desktop_workspace_projects','detail_generator_jobs','detail_generator_design_resources','detail_generator_knowledge_revisions']; t text;\nBEGIN\n IF current_setting('jiansun.import_xmin') !~ '^[0-9]+$' THEN RAISE EXCEPTION 'Original import transaction identity is required'; END IF;\n FOREACH t IN ARRAY tables LOOP\n IF to_regclass('public.'||t) IS NOT NULL THEN EXECUTE format('LOCK TABLE public.%I IN SHARE MODE',t); END IF;\n END LOOP;\n FOR d IN ${desired(rows)} LOOP\n IF NOT EXISTS(SELECT 1 FROM public.detail_generator_brand_profiles p WHERE p.id=d.id AND p.name=d.name AND p.revision=1 AND p.profile=d.profile AND p.xmin::text=current_setting('jiansun.import_xmin')) THEN RAISE EXCEPTION 'Imported row is missing or has changed: %',d.name; END IF;\n FOR r IN SELECT table_name,column_name,data_type FROM information_schema.columns WHERE table_schema='public' AND table_name=ANY(tables) AND (data_type IN ('json','jsonb') OR column_name IN ('resource_id','brand_profile_id','profile_id','brandProfileId','profileId')) LOOP\n IF r.data_type IN ('json','jsonb') THEN\n EXECUTE format('SELECT EXISTS(SELECT 1 FROM public.%I WHERE jsonb_path_exists(%I::jsonb, ''$.** ? (@ == $target)'', jsonb_build_object(''target'', $1)))',r.table_name,r.column_name) INTO used USING d.id;\n ELSE\n EXECUTE format('SELECT EXISTS(SELECT 1 FROM public.%I WHERE %I::text=$1)',r.table_name,r.column_name) INTO used USING d.id;\n END IF;\n IF used THEN RAISE EXCEPTION 'Brand is referenced by %.%; refusing rollback: %',r.table_name,r.column_name,d.name; END IF;\n END LOOP;\n END LOOP;\nEND $rollback$;\nDELETE FROM public.detail_generator_brand_profiles p USING (${desired(rows)}) d WHERE p.id=d.id AND p.name=d.name AND p.revision=1 AND p.profile=d.profile AND p.xmin::text=current_setting('jiansun.import_xmin') RETURNING p.id,p.name,p.revision;\nCOMMIT;\n` return {dryRun,apply,verify,rollback} } export function prepareImport({packBytes,migrationBytes,originals,batchId=randomUUID()}){ check(/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(batchId),'Import batch UUID is invalid') check(sha(packBytes)===SOURCE_PACK_SHA256,'已验收知识包的字节哈希变化,停止准备') if(migrationBytes)check(sha(migrationBytes)===SOURCE_MIGRATION_SHA256,'20261013验收SQL哈希变化,停止准备') for(const [file,digest]of Object.entries(SOURCE_FILES))check(originals[file]&&sha(originals[file])===digest,'企业知识原文哈希变化或文件缺失:'+file) const pack=JSON.parse(packBytes.toString('utf8')) check(pack.version===SOURCE_VERSION&&Array.isArray(pack.brands)&&pack.brands.length===4,'知识包版本或品牌数量变化') const rows=BRANDS.map(name=>{const profile=pack.brands.find(p=>p.name===name);validateBrand(profile);return {id:'enterprise-brand-'+batchId+'-'+sha(name).slice(0,8),name,revision:1,profile:structuredClone(profile)}}) check(new Set(rows.map(r=>r.id)).size===4,'Import IDs are duplicated') const bundle={format:'jiansun-brand-import-v1',batchId,sourceVersion:SOURCE_VERSION,sourcePackSha256:SOURCE_PACK_SHA256,sourceMigrationSha256:SOURCE_MIGRATION_SHA256,originalFiles:SOURCE_FILES,rows} const bytes=Buffer.from(JSON.stringify(bundle,null,2)+'\n');const packageSha=sha(bytes) return {bundle,bytes,packageSha,sql:sqlPackage(rows,packageSha)} } async function main(){ const args=process.argv.slice(2);const options={};for(let n=0;n({id:row.id,name:row.name,revision:row.revision,profileSha256:sha(JSON.stringify(row.profile)),visualRulesSha256:sha(JSON.stringify(row.profile.visualRules))})),artifacts:Object.fromEntries(Object.entries(files).map(([name,bytes])=>[name,sha(bytes)])),notes:['Profiles retain original draft status and source adaptation statement.','Hex palette values were originally adapted from colour names; this import does not assert brand approval.','No new schema or knowledge management route is required.','Apply conflicts fail the transaction; no overwrite or ON CONFLICT update.','Rollback requires the exact original xmin and unmodified revision 1 payload; referenced rows cannot be removed.']} await fs.writeFile(path.join(out,'manifest.json'),JSON.stringify(manifest,null,2)+'\n',{flag:'wx'});console.log(JSON.stringify({prepared:true,output:out,packageSha256:prepared.packageSha,rows:manifest.brands,databaseConnections:0,productionWrites:0},null,2)) } if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url))await main()