Files
SGF/scripts/check-sgf-solution.mjs
T
2026-11-11 09:00:17 +02:00

57 lines
6.8 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import assert from 'node:assert/strict';
import {spawnSync} from 'node:child_process';
// Internal release gate, not a product menu. Read-only metadata; never bootstrap/migrate an application.
const contractFile='docs/integration/sgf-solution-contract.json',contract=JSON.parse(fs.readFileSync(contractFile));
assert.equal(contract.sgc_required,false);assert.equal(contract.new_demonstrations,false);
assert.deepEqual(contract.modules.map(x=>x.id),['commercial','purchasing','stock','workshop','finance','accounting','human_resources']);
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'));
assert.equal(run.id,'2576427d1939');assert.equal(run.port,55439);
const root=path.join(run.root,'private/receipt-composition-cc50a276c10c'),state=JSON.parse(fs.readFileSync(path.join(root,'state.json'))),server=JSON.parse(fs.readFileSync(path.join(root,'demo-server.json')));
assert.equal(state.tag,'cc50a276c10c');assert.equal(server.port,8085);
const hash=file=>crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
// Keep the footprint tied to real declarations, rather than guessed prefixes.
const declarations=['database/migrations','database/sql','packages/payables/src','packages/general-ledger/src'].flatMap(dir=>fs.readdirSync(dir).filter(f=>/\.(php|sql)$/.test(f)).map(f=>fs.readFileSync(path.join(dir,f),'utf8'))).join('\n')+'\n'+fs.readFileSync('scripts/sgf-document-cancellation-schema.php','utf8')+'\n'+fs.readFileSync('app/Shared/Installation/HumanResourcesProfiles.php','utf8');
const known=new Set(['users','companies','units','permission_grants','a_clients','a_services','a_service_drafts','a_billing_drafts',
...[...declarations.matchAll(/(?:CREATE TABLE\s+|Schema::create\(['"])([a-z0-9_]+)/gi)].map(x=>x[1])]);
for(const m of contract.modules){for(const table of m.required_host_tables)assert(known.has(table),'Undeclared table '+table);for(const file of [...m.required_source_files,...m.proofs])assert(fs.existsSync(file),'Missing recorded source/evidence '+file);}
const sql=`BEGIN READ ONLY;
SELECT json_build_object('db',current_database(),'role',current_user,'cluster',current_setting('cluster_name'),'elevated',rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls) FROM pg_roles WHERE rolname=current_user;
SELECT json_build_object('tables',json_agg(tablename ORDER BY tablename)) FROM pg_tables WHERE schemaname='public';
COMMIT;`;
const result=spawnSync('C:/Program Files/PostgreSQL/17/bin/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',state.env.DB_USERNAME,'-d',state.env.DB_DATABASE,'-Atq','-v','ON_ERROR_STOP=1'],{input:sql,encoding:'utf8',windowsHide:true,env:{...process.env,PGPASSWORD:state.env.DB_PASSWORD}});
if(result.status!==0)throw Error('Selected presentation metadata unavailable; no ready result may be inferred');
const [identity,metadata]=result.stdout.trim().split(/\r?\n/).map(JSON.parse);
assert.equal(identity.db,'budget_core_sgf_cc50a276c10c');assert.equal(identity.role,'ir_composition_cc50a276c10c');assert.equal(identity.cluster,'sgf_modular_regression_'+run.id);assert.equal(identity.elevated,false);
const tables=new Set(metadata.tables);
const native=state.nativeConnection;
assert.equal(native.database,'compras_receipts_test_cc50a276c10c');assert.equal(native.username,'payable_history_native_cc50a276c10c');
const nr=spawnSync('C:/Program Files/PostgreSQL/17/bin/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',native.username,'-d',native.database,'-Atq','-v','ON_ERROR_STOP=1'],{input:sql,encoding:'utf8',windowsHide:true,env:{...process.env,PGPASSWORD:native.password}});
assert.equal(nr.status,0,'Canonical native book metadata unavailable');const [ni,nm]=nr.stdout.trim().split(/\r?\n/).map(JSON.parse);assert.equal(ni.db,native.database);assert.equal(ni.role,native.username);assert.equal(ni.cluster,identity.cluster);assert.equal(ni.elevated,false);const nativeTables=new Set(nm.tables);
const modules=contract.modules.map(module=>({id:module.id,label:module.label,features:module.features,
sources:module.required_source_files.map(file=>({file,present:fs.existsSync(file),sha256:fs.existsSync(file)?hash(file):null})),
existing_evidence:module.proofs.map(file=>({file,present:fs.existsSync(file),sha256:fs.existsSync(file)?hash(file):null,not_reexecuted:true})),
missing_host_tables:module.required_host_tables.filter(x=>!tables.has(x)),
missing_native_tables:(module.required_native_tables??[]).filter(x=>!nativeTables.has(x)),
integrated_release_verified:false,client_presentable:false,remaining:module.work_remaining}));
const purchasing=JSON.parse(fs.readFileSync('docs/proofs/purchasing-completion-final-20261104-r2.json'));
modules.find(m=>m.id==='purchasing').accepted_component_journey=purchasing.passed===true&&purchasing.installation.id===state.tag
&&purchasing.sources.every(source=>fs.existsSync(path.join(state.app,source.file))&&hash(path.join(state.app,source.file))===source.sha256);
const acceptance=fs.existsSync(contract.acceptance_proof)?JSON.parse(fs.readFileSync(contract.acceptance_proof)):null;
// No file-presence/schema/table-count shortcut can declare the six modules accepted.
const ready=acceptance?.passed===true&&acceptance.contract_sha256===hash(contractFile)&&acceptance.installation==='cc50a276c10c'
&&contract.release_gates.every(g=>acceptance.gates?.[g.id]===true)
&&modules.every(m=>m.sources.every(s=>s.present)&&m.missing_host_tables.length===0&&m.missing_native_tables.length===0)
&&contract.modules.every(m=>acceptance.modules?.[m.id]?.same_installation===true&&m.features.every(f=>acceptance.modules[m.id].features?.[f]===true));
const output={date:'2026-11-11',contract_sha256:hash(contractFile),installation:'cc50a276c10c',port:8085,sgc_dependency:false,
full_product_complete:(contract.product_backlog?.length>0&&contract.product_backlog.every(x=>x.implemented===true))??false,product_backlog:contract.product_backlog??[],
modules,release_gates:contract.release_gates.map(g=>({...g,passed:ready?true:false})),client_presentable:ready,
limits:['Host-table footprint is a minimum projection, not a full migration/dependency validation.','Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.','Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready.'],
database_writes:0,application_bootstraps:0,cache_cleared:false,new_demo:false,new_rights:false};
const out=process.argv[2];if(out){assert(/^docs\/proofs\/[a-z0-9-]+\.json$/.test(out)&&!fs.existsSync(out),'Fresh proof required');fs.writeFileSync(out,JSON.stringify(output,null,2)+'\n');}
console.log(JSON.stringify({modules:modules.length,client_presentable:ready,modules_missing_host_structures:modules.filter(m=>m.missing_host_tables.length).map(m=>m.id),database_writes:0,new_demo:false,output:out??null}));
if(!out&&!ready)process.exitCode=2;