From 4a94e8a05455126a0296dcced911265a1743ffd3 Mon Sep 17 00:00:00 2001 From: nala Date: Wed, 28 Oct 2026 14:04:53 +0200 Subject: [PATCH] Preserve authorised industrial installation record and guarded installer --- AGENTS.md | 2 + docs/INDUSTRIAL_COST_DELIVERY.md | 16 ++++ scripts/industrial-cost-dev-install.php | 102 ++++++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 scripts/industrial-cost-dev-install.php diff --git a/AGENTS.md b/AGENTS.md index ae74989..051f330 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # SGF Tempo — instruções locais +- Autorização específica de 28/10/2026 executada: apenas `100000` aplicada em `sgf_tempo_dev`, por caminho explícito com `sgf_dev_owner`, e quatro concessões auditadas no tenant #1 / Tempo Lda #1 / SEGI #1: conta #1 `industrial_costs.view/prepare`; conta #2 `industrial_costs.view/validate`. Ver actualização de `docs/INDUSTRIAL_COST_DELIVERY.md`. Preservadas 66 tabelas anteriores e acessos; oito tabelas industriais vazias. Aplicação mantém `sgf_dev_app`; custeio industrial/directo, oficinas, recebimentos e emissão reais bloqueados, com guardas SQL verificadas. Sem utilização real ou publicação. Indicações anteriores de `100000`/concessões por autorizar são históricas; esta instalação não activa operações reais. + - Aceitação específica de 28/10/2026: o utilizador aceitou `INDUSTRIAL-COST-SIM-v1` e autorizou checkpoint Git exclusivamente local, sem credenciais ou dados reais. Ver `docs/INDUSTRIAL_COST_ACCEPTANCE.md`. Substitui apenas a pendência de aceitação/encerramento técnico do ensaio. Migração `100000`, quatro concessões e utilização real permanecem por autorizar; sem aplicação, activação operacional, publicação ou novo incremento. - Autorização técnica de 27/10/2026: `INDUSTRIAL-COST-SIM-v1`, secções 15/15.5, só em `sgf_tempo_test`. Ver `docs/INDUSTRIAL_COST_DELIVERY.md`. Um lote/produto sintéticos, material 1000 + trabalho 500 + CIF 300 = 1800, 10 UN / 180 por UN; conjunto 600, capacidade 10 HM, uso 5 HM e residual 300 separado. Sem taxa material, 800 conhecidos, total/unitário indisponíveis. Critério é hipótese sintética, não método real aprovado. Preparação/validação industrial explícitas por identidades distintas; não herdar custo directo, transformar oficina em lote ou duplicar origens. `100000` e quatro concessões reais apenas propostas, NÃO aplicar sem confirmação específica. Preservar checkpoints/alterações/aprovações/bloqueios. Sem aceitação presumida, checkpoint de encerramento, stock, previsto/realizado, margens, lançamentos ou publicação. diff --git a/docs/INDUSTRIAL_COST_DELIVERY.md b/docs/INDUSTRIAL_COST_DELIVERY.md index 3cce9bc..387861e 100644 --- a/docs/INDUSTRIAL_COST_DELIVERY.md +++ b/docs/INDUSTRIAL_COST_DELIVERY.md @@ -1,5 +1,21 @@ # INDUSTRIAL-COST-SIM-v1 — entrega técnica isolada +## Aplicação técnica especificamente autorizada — 28/10/2026 + +Após apresentação do destino e das quatro concessões exactas, o utilizador autorizou apenas a migração `100000` em `sgf_tempo_dev` e essas concessões, preservando dados, acessos e bloqueios, sem utilização real ou publicação. As referências posteriores a migração/concessões não aplicadas descrevem o estado histórico de 27/10 e da aceitação anterior à autorização específica. + +Executado `scripts/sgf.ps1 php scripts/industrial-cost-dev-install.php --apply-confirmed-100000-and-four-grants`. Instalador confere destino `sgf_tempo_dev`, porta 55432, proprietário `sgf_dev_owner` sem poderes de superutilizador/criação de bases/papéis/bypass RLS, e migra apenas o caminho `database/migrations/2026_10_27_100000_create_industrial_cost_simulation.php`. Aplicação permanece `sgf_dev_app`. + +- Apenas uma migração acrescentada; instalações anteriores `070000`/`080000`/`090000` preservadas. +- 66 tabelas anteriores conferidas por contagem e impressão digital dos registos, sem exportar conteúdos; dados e acessos anteriores preservados. Só acrescentadas a migração, quatro concessões e quatro entradas de auditoria correspondentes. +- Tenant #1 / Tempo Lda #1 / SEGI #1: conta #1, `industrial_costs.view` e `industrial_costs.prepare`; conta #2, `industrial_costs.view` e `industrial_costs.validate`. Identidades e pertenças existentes conferidas; sem direitos de gestão ou outras concessões. +- Oito tabelas industriais vazias. Listagens das duas contas continuam bloqueadas, sem lotes. +- Guarda, abertura, dados, cálculo e decisão industriais recusam com 403; também recusam as guardas de custo directo, oficinas, recebimentos e emissão. Funções SQL de ambiente industrial/directo/recebimentos/oficinas recusam a base de desenvolvimento. Fontes funcionais e guardas não alteradas. + +Prova de execução mantida localmente em `storage/app/private/industrial-cost-dev-install-proof.json`, sem conteúdos dos registos ou credenciais. Validação de sintaxe PHP aprovada; verificações de preservação e bloqueios executadas pelo instalador. Não se repetiram as provas sintéticas aceites, nem se recriou a base de testes. Sem taxas, critérios, lotes, cálculos ou operações reais; nenhuma publicação. A aceitação técnica de `cbdf2b4` permanece preservada. + +Na revisão para o checkpoint local, o código do instalador foi desidentificado: nomes e emails usados na conferência inicial foram retirados, conservando IDs autorizados, existência das contas, pertenças e verificações de âmbito. Sintaxe novamente validada, sem reexecutar a instalação. Ficheiros privados, credenciais e conteúdos dos registos não integram o checkpoint. + **Actualização de 28/10/2026:** entrega aceite expressamente pelo utilizador, com autorização de checkpoint Git exclusivamente local. Ver [INDUSTRIAL_COST_ACCEPTANCE.md](INDUSTRIAL_COST_ACCEPTANCE.md). As referências abaixo a aceitação/encerramento pendentes descrevem o estado original da entrega de 27/10. Migração `100000`, quatro concessões e utilização real permanecem por autorizar. 27/10/2026, Africa/Johannesburg. Implementação delimitada pelas secções 15 e 15.5 de [INDUSTRIAL_COST_ACCOUNTING_SCOPE.md](INDUSTRIAL_COST_ACCOUNTING_SCOPE.md). **Sem aceitação presumida ou checkpoint de encerramento.** Preservados `d10ba06`, `7628645`, alterações posteriores, oficinas, recebimentos, cálculo fiscal e aprovações existentes. Não se acedeu ao InvesteMoz nem se repetiu investigação fiscal. diff --git a/scripts/industrial-cost-dev-install.php b/scripts/industrial-cost-dev-install.php new file mode 100644 index 0000000..4475c24 --- /dev/null +++ b/scripts/industrial-cost-dev-install.php @@ -0,0 +1,102 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Artisan; +use App\Modules\Identity\Services\LocalDatabaseGuard; +use App\Modules\Identity\Services\Permissions; +use App\Modules\Identity\Services\Audit; +use App\Modules\Industry\IndustrialCosts; +use App\Modules\Workshop\WorkshopWorkflow; +use App\Shared\Context\CompanyContext; + +function ensureIndustrialCost(bool $ok,string $message): void { if(!$ok)throw new RuntimeException($message); } +function industrialCostFingerprint(string $table,?int $max=null): array { + ensureIndustrialCost((bool)preg_match('/^[a-z_]+$/D',$table),'Unexpected table identifier.'); + $where=$max===null?'':' WHERE id <= '.$max; + $r=DB::selectOne('SELECT count(*) AS n, md5(COALESCE(string_agg(row_to_json(t)::text, E\'\\n\' ORDER BY row_to_json(t)::text),\'\')) AS h FROM (SELECT * FROM "'.$table.'"'.$where.') t'); + return [(int)$r->n,$r->h]; // In memory only; no record contents exported. +} +ensureIndustrialCost(LocalDatabaseGuard::allows(),'Own SGF development application connection required.'); +ensureIndustrialCost($argv===[$argv[0],'--apply-confirmed-100000-and-four-grants'],'Exact confirmed operation argument required.'); +$migration='2026_10_27_100000_create_industrial_cost_simulation'; +ensureIndustrialCost(DB::table('migrations')->where('migration','2026_10_27_090000_create_workshop_direct_costs')->exists(),'Expected direct cost installation missing.'); +ensureIndustrialCost(!DB::table('migrations')->where('migration',$migration)->exists(),'100000 already installed; review changed state.'); +ensureIndustrialCost(DB::table('migrations')->where('migration','2026_10_27_080000_create_workshop_resource_records')->exists(),'Expected workshop installation missing.'); +ensureIndustrialCost(DB::table('migrations')->where('migration','2026_10_27_070000_create_simulated_receivables')->exists(),'Expected receivables installation missing.'); +$newTables=['ic_products','ic_lots','ic_inputs','ic_results','ic_decisions','ic_commands','ic_events','ic_outbox']; +$tables=array_map(fn($r)=>$r->tablename,DB::select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename")); +ensureIndustrialCost(!array_intersect($tables,$newTables),'Industrial tables already exist; review first.'); +$rights=[1=>['view','prepare'],2=>['view','validate']]; +$checkScope=function(): void { + // Account IDs are the specifically authorised scope; personal details stay outside source control. + foreach([1,2] as $id){ + ensureIndustrialCost(DB::table('users')->where('id',$id)->exists(),'Confirmed account missing.'); + ensureIndustrialCost(DB::table('company_memberships')->where(['user_id'=>$id,'tenant_id'=>1,'company_id'=>1])->exists(),'Existing membership required.'); + $p=app(Permissions::class);$p->require($id,1,1,'company.view');$p->require($id,1,1,'unit.view',1); + } + ensureIndustrialCost(DB::table('companies')->where(['id'=>1,'tenant_id'=>1,'name'=>'Tempo Lda'])->exists(),'Confirmed company differs.'); + ensureIndustrialCost(DB::table('units')->where(['id'=>1,'tenant_id'=>1,'company_id'=>1,'name'=>'SEGI'])->exists(),'Confirmed unit differs.'); + ensureIndustrialCost(!DB::table('permission_grants')->where('permission','like','industrial_costs.%')->exists(),'Unexpected existing industrial cost rights; review first.'); +}; +$checkScope(); +$baseline=[];foreach($tables as $t)$baseline[$t]=industrialCostFingerprint($t); +$max=[];foreach(['permission_grants','identity_audit'] as $t)$max[$t]=(int)DB::table($t)->max('id'); +$ledger=DB::table('migrations')->orderBy('id')->get()->map(fn($r)=>(array)$r)->all(); +$sourceHashes=[];foreach(['app/Modules/Industry/IndustrialCosts.php','app/Modules/Workshop/DirectCosts.php','app/Modules/Services/ServiceReceivables.php','app/Modules/Services/ServiceInvoicing.php','app/Modules/Workshop/WorkshopWorkflow.php'] as $p)$sourceHashes[$p]=hash_file('sha256',base_path($p)); +$secrets=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/database.json'),true,32,JSON_THROW_ON_ERROR); +$owner=config('database.connections.'.config('database.default')); +$owner['username']='sgf_dev_owner';$owner['password']=$secrets['dev_owner'];$owner['url']=null; +config(['database.connections.sgf_industrial_cost_owner'=>$owner]);unset($owner,$secrets); +try { + $connection=DB::connection('sgf_industrial_cost_owner'); + $identity=$connection->selectOne('SELECT current_database() AS db,current_user AS role,inet_server_port() AS port,rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user'); + ensureIndustrialCost($identity->db==='sgf_tempo_dev'&&$identity->role==='sgf_dev_owner'&&(int)$identity->port===55432&&!$identity->rolsuper&&!$identity->rolcreatedb&&!$identity->rolcreaterole&&!$identity->rolbypassrls,'Unexpected migration owner/destination.'); + $connection->transaction(function()use($migration,$baseline,$newTables): void { + $applicationConnection=DB::getDefaultConnection();DB::setDefaultConnection('sgf_industrial_cost_owner'); + try { + $exit=Artisan::call('migrate',['--database'=>'sgf_industrial_cost_owner','--path'=>'database/migrations/'.$migration.'.php','--force'=>true,'--no-interaction'=>true]); + ensureIndustrialCost($exit===0,'Targeted migration failed.'); + foreach($baseline as $t=>$fp)if($t!=='migrations')ensureIndustrialCost(industrialCostFingerprint($t)===$fp,'Existing records changed during migration: '.$t); + foreach($newTables as $t)ensureIndustrialCost(DB::table($t)->count()===0,'New tables must be empty.'); + } finally { DB::setDefaultConnection($applicationConnection); } + }); +} finally { DB::purge('sgf_industrial_cost_owner');config(['database.connections.sgf_industrial_cost_owner'=>null]); } +ensureIndustrialCost(LocalDatabaseGuard::allows(),'Application must remain sgf_dev_app.'); +$afterLedger=DB::table('migrations')->orderBy('id')->get()->map(fn($r)=>(array)$r)->all(); +ensureIndustrialCost(count($afterLedger)===count($ledger)+1&&array_slice($afterLedger,0,count($ledger))===$ledger&&end($afterLedger)['migration']===$migration,'Unexpected migration ledger delta.'); +$denials=[]; +$grants=DB::transaction(function()use($rights,$checkScope,$baseline,$max,&$denials): array { + DB::table('companies')->where('id',1)->lockForUpdate()->firstOrFail();$checkScope();$grants=[]; + foreach($rights as $user=>$actions)foreach($actions as $action){ + $g=['user_id'=>$user,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'permission'=>'industrial_costs.'.$action]; + DB::table('permission_grants')->insert($g+['created_at'=>now(),'updated_at'=>now()]); + Audit::record('industrial_costs.initial_grant',null,1,1,1,subject:$user,details:['permission'=>$g['permission'],'reason'=>'Four unit grants specifically authorised; real operations remain blocked.']);$grants[]=$g; + } + foreach($baseline as $t=>$fp)if($t!=='migrations')ensureIndustrialCost(industrialCostFingerprint($t,$max[$t]??null)===$fp,'Prior records changed: '.$t); + ensureIndustrialCost(DB::table('permission_grants')->where('id','>',$max['permission_grants'])->count()===4,'Exactly four new grants required.'); + ensureIndustrialCost(DB::table('identity_audit')->where('id','>',$max['identity_audit'])->count()===4&&DB::table('identity_audit')->where('id','>',$max['identity_audit'])->where('action','industrial_costs.initial_grant')->count()===4,'Exactly four grant audit entries required.'); + $actual=DB::table('permission_grants')->where('permission','like','industrial_costs.%')->get(['user_id','tenant_id','company_id','unit_id','permission'])->map(fn($r)=>(array)$r)->all(); + $sort=fn($a,$b)=>[$a['user_id'],$a['permission']]<=>[$b['user_id'],$b['permission']];usort($actual,$sort);$expected=$grants;usort($expected,$sort);ensureIndustrialCost($actual===$expected,'Granted accounts/rights/scope differ.'); + foreach([1,2] as $actor){app(CompanyContext::class)->selectFor($actor,1,1,1);$listing=app(IndustrialCosts::class)->listing($actor);ensureIndustrialCost($listing['blocked']&&$listing['lots']===[],'Normal industrial cost listing must remain blocked.');} + foreach(['guard','open','input','calculate','decide','direct','workshop','receivables','issuance'] as $entry){ + try { + $r=app(IndustrialCosts::class); + match($entry){'guard'=>IndustrialCosts::guard(),'open'=>$r->open(1,[],'blocked-probe'),'direct'=>\App\Modules\Workshop\DirectCosts::guard(),'input'=>$r->input(0,1,[],'blocked-probe'),'calculate'=>$r->calculate(0,1,[],'blocked-probe'),'decide'=>$r->decide(0,2,[],'blocked-probe'),'workshop'=>WorkshopWorkflow::guard(),'receivables'=>\App\Modules\Services\ServiceReceivables::guard(),'issuance'=>\App\Modules\Services\ServiceInvoicing::guard()}; + throw new RuntimeException('Operational entry did not refuse.'); + }catch(\Symfony\Component\HttpKernel\Exception\HttpException $e){ensureIndustrialCost($e->getStatusCode()===403,'Expected operational refusal.');$denials[$entry]=403;} + } + app(CompanyContext::class)->selectFor(1,1,1,1);$w=app(WorkshopWorkflow::class)->listing(1);ensureIndustrialCost($w['blocked']&&$w['orders']===[],'Workshop operational block must remain.'); + return $grants; +}); +foreach(['ic','dc','sr','wo'] as $prefix){ + try {DB::select('SELECT sgf_'.$prefix.'_environment()');throw new RuntimeException('SQL environment guard did not refuse.');} + catch(\Illuminate\Database\QueryException $e){ensureIndustrialCost(str_contains($e->getMessage(),'SGF_'.strtoupper($prefix).'_TEST_DATABASE_ONLY'),'Unexpected SQL guard response.');} +} +foreach($sourceHashes as $p=>$hash)ensureIndustrialCost(hash_file('sha256',base_path($p))===$hash,'Functional source or guard changed.'); +foreach($newTables as $t)ensureIndustrialCost(DB::table($t)->count()===0,'New industrial cost tables must remain empty.'); +$proof=['date'=>'2026-10-28','database'=>'sgf_tempo_dev','migration_applied'=>$migration,'only_one_migration_added'=>true,'workshop_080000_preserved'=>true,'preexisting_tables_checked'=>count($baseline),'prior_records_and_accesses_preserved'=>true,'new_empty_tables'=>$newTables,'new_grants'=>$grants,'audited_new_grants'=>4,'application_role'=>'sgf_dev_app','both_normal_account_listings_blocked'=>true,'entry_denials'=>$denials,'cost_receivables_and_workshop_sql_guards_refused'=>true,'functional_and_guard_sources_unchanged'=>true,'real_operations_created'=>false]; +file_put_contents(storage_path('app/private/industrial-cost-dev-install-proof.json'),json_encode($proof,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); +echo 'Applied only 100000 and four audited grants; '.count($baseline)." prior tables preserved, eight new tables empty, costing, workshop and operational/fiscal guards verified.\n";