Files
SGF/scripts/summarize-stock-receipt.mjs
T
2026-11-02 20:29:34 +02:00

55 lines
8.2 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import {spawnSync} from 'node:child_process';
const read=p=>JSON.parse(fs.readFileSync(p,'utf8').replace(/^\uFEFF/,''));
const hash=p=>crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
const state=read('C:/laragon/data/sgf-tempo/run/modular-regression-current.json');
const [tag,affectedTag,lotsTag]=process.argv.slice(2,5),sgfFile=path.resolve(process.argv[5]||''),functionalXml=path.resolve(process.argv[6]||'');
for(const t of [tag,affectedTag,lotsTag])if(!/^[a-f0-9]{12}$/.test(t||''))throw Error('Unexpected QA tag');
for(const p of [sgfFile,functionalXml])if(path.dirname(p)!==path.resolve(state.root)||!/^stock-read-\d+\.(json|xml)$/.test(path.basename(p)))throw Error('Unexpected private SGF proof');
const root=t=>path.join(state.root,'private','stock-receipt-'+t);
const native=read(path.join(root(tag),'receipt-results.json')),sgf=read(sgfFile),source=read(path.join(root(tag),'source-connection.json'));
if(!sgf.same_native_movement_observed||!sgf.source_write_refused||!sgf.source_connection_failure_503_and_recovery||!native.fault_atomic_and_recovered||!native.concurrency.one_movement)throw Error('Incomplete link proof');
const artifact='../compras/packages/inventory-write/releases/0.1.0',manifest=read(artifact+'/manifest.json');
if(hash(artifact+'/manifest.json')!==native.writer_manifest_sha256||native.writer_manifest_sha256!==sgf.writer_manifest_sha256)throw Error('Writer artifact mismatch');
for(const [file,sha] of Object.entries(manifest.files))if(hash(artifact+'/'+file)!==sha||hash('packages/inventory-write/'+file)!==sha)throw Error('Writer source differs');
const junit=p=>{
const xml=fs.readFileSync(p,'utf8'),suite=xml.match(/<testsuite\b([^>]*)>/)?.[1];
const result=Object.fromEntries(['tests','assertions','errors','failures'].map(k=>[k,Number(suite?.match(new RegExp('\\b'+k+'="(\\d+)"'))?.[1])]));
if(!result.tests||result.errors!==0||result.failures!==0)throw Error('Incomplete JUnit');return {...result,sha256:hash(p)};
};
const previous=read('docs/proofs/module-association-http-lock-summary-20261102.json');
for(const p of [previous.proof,...previous.previous_proofs])if(hash(p.path)!==p.sha256)throw Error('Prior association proof changed');
const oldLoad=read('docs/proofs/stock-read-load-20261102.json');
if(hash('packages/inventory-read/src/InventoryReader.php')!==oldLoad.source_sha256)throw Error('Reader SQL changed since valid load proof');
const pg=(role,password,sql)=>{
const r=spawnSync('C:/Program Files/PostgreSQL/17/bin/psql.exe',['-h','127.0.0.1','-p','55439','-U',role,'-d','compras_receipts_test_'+tag,'-At','-v','ON_ERROR_STOP=1'],{encoding:'utf8',windowsHide:true,env:{...process.env,PGPASSWORD:password},input:'BEGIN READ ONLY; '+sql+'; ROLLBACK;'});
if(r.status!==0)throw Error('Read-only source postcheck failed');return JSON.parse(r.stdout.split(/\r?\n/).find(l=>l.startsWith('{')));
};
const roleCheck=pg(source.username,source.password,"SELECT json_build_object('database',current_database(),'role',current_user,'cluster',current_setting('cluster_name'),'default_read_only',current_setting('default_transaction_read_only'),'elevated',rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls,'stock_select',has_table_privilege(current_user,'stock_movements','SELECT'),'stock_write',has_table_privilege(current_user,'stock_movements','INSERT,UPDATE,DELETE'),'receipt_select',has_table_privilege(current_user,'goods_receipts','SELECT')) FROM pg_roles WHERE rolname=current_user");
if(roleCheck.database!=='compras_receipts_test_'+tag||roleCheck.role!=='stock_receipt_read_'+tag||roleCheck.cluster!=='sgf_modular_regression_'+state.id||roleCheck.elevated||roleCheck.stock_write||roleCheck.receipt_select||!roleCheck.stock_select||roleCheck.default_read_only!=='on')throw Error('Reader role not restricted');
const env=fs.readFileSync(path.join(root(tag),'app/.env.testing'),'utf8'),pw=env.match(/^DB_PASSWORD=([a-f0-9]+)$/m)?.[1];if(!pw)throw Error('Private QA credentials unavailable');
const tenant=native.link.tenant;if(!Number.isSafeInteger(tenant)||tenant<1)throw Error('Unexpected QA tenant');
const book=pg('stock_receipt_qa_'+tag,pw,`SELECT json_build_object('movements',(SELECT count(*) FROM stock_movements WHERE tenant_id=${tenant}),'physical_milli',(SELECT sum(quantity_milli)::text FROM stock_movements WHERE tenant_id=${tenant}),'receipt_confirmations',(SELECT count(*) FROM audit_logs WHERE tenant_id=${tenant} AND action='receipt.confirmed'),'author_and_date_present',(SELECT bool_and(user_id IS NOT NULL AND created_at IS NOT NULL) FROM audit_logs WHERE tenant_id=${tenant} AND action='receipt.confirmed'),'movement_origin_hash',(SELECT snapshot::jsonb->>'inventory_source_hash' FROM stock_movements WHERE tenant_id=${tenant} LIMIT 1))`);
if(book.movements!==1||book.physical_milli!=='4000'||book.receipt_confirmations!==2||!book.author_and_date_present||!/^[a-f0-9]{64}$/.test(book.movement_origin_hash))throw Error('Source evidence changed');
const post=spawnSync('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['scripts/stock-read-state.php'],{encoding:'utf8',windowsHide:true});if(post.status!==0)throw Error('Temporary SGF permissions not restored');
const paths=['packages/inventory-write/src/ReceiptMovementWriter.php','app/Modules/Inventory/StockReadController.php','../compras/app/Services/ReceiptWorkflow.php','../compras/app/Services/ReceiptStockMovement.php'];
const report={date:'2026-11-02',increment:'COMMON-RECEIPT-STOCK-SIM-v1',status:'native_receipt_shared_effect_and_sgf_read_verified',
tests:{native_link:junit(path.join(root(tag),'linked-junit.xml')),affected_receipts:junit(path.join(root(affectedTag),'affected-junit.xml')),affected_lot_receipts:junit(path.join(root(lotsTag),'lots-junit.xml')),
final_native_adapter_replay:junit(path.join(root(tag),'replay-junit.xml')),sgf_functional:junit(functionalXml),sgf_actual_book_link:junit(sgfFile.replace(/\.json$/,'.xml'))},
scenario:{first_received_milli:6000,first_accepted_milli:4000,first_rejected_milli:2000,second_received_milli:2000,second_accepted_milli:0,second_rejected_milli:2000,final_physical_milli:'4000',movement_count:1},
source_hash_stale_refused:native.source_version_stale_refused,core_replay_same_movement:native.core_replay_same_movement,fault_atomic_and_recovered:native.fault_atomic_and_recovered,concurrency:native.concurrency,
same_native_movement_observed_by_sgf:sgf.same_native_movement_observed,read_does_not_copy_or_mutate_stock:sgf.native_book_unchanged_by_read&&sgf.sgf_book_unchanged_by_read,source_connection_failure_503_and_recovery:true,
package:{...manifest,manifest_sha256:native.writer_manifest_sha256,canonical_and_artifact_identical:true},source_hashes:Object.fromEntries(paths.map(p=>[p,hash(p)])),
source_postcheck:{reader:roleCheck,book},sgf_postcheck:JSON.parse(post.stdout),
preservation:{new_qa_databases_only:true,fixtures_retained:true,real_or_demo_database_connected:false,protected_six_records_untouched:true,blocked_cache_untouched:true,prior_association_proofs_unchanged:true,
purchasing_candidates_activated:false,public_rights_changed:false,publication:false,checkpoint:false},
reused_proofs:{stock_load_sha256:hash('docs/proofs/stock-read-load-20261102.json'),stock_composition_sha256:hash('docs/proofs/stock-read-composition-20261102.json')},
limits:['Existing native receipt validation remains authoritative; no independent SGF receipt preparation/confirmation UI or command delivered.',
'Source connection is confined to the local QA book and a SELECT-only role; it is not a required remote connector or production architecture.',
'StockWorkflow issues/transfers/corrections and workshop consumption are not extracted by this receipt-only writer.',
'PostgreSQL tested; other database drivers not validated in this increment. No new load benchmark or deployment capacity claim.'],visual_review_pending:true,deployment_capacity_validated:false};
fs.writeFileSync('docs/proofs/stock-receipt-link-20261102.json',JSON.stringify(report,null,2)+'\n');
console.log(JSON.stringify({tests:report.tests,physical_milli:book.physical_milli,movements:book.movements,receipt_audits:book.receipt_confirmations,source_read_only:true,sgf_temporary_stock_grants:report.sgf_postcheck.temporary_stock_grants_remaining},null,2));