This commit is contained in:
2026-11-02 20:27:11 +02:00
parent b44378b899
commit badaada043
300 changed files with 17458 additions and 1056 deletions
+200
View File
@@ -0,0 +1,200 @@
<?php
use App\Models\Tenant;
use App\Models\User;
use App\Services\ConflictGuard;
use App\Services\OrganizationAccess;
use App\Services\OrganizationActivationPreview;
use App\Services\OrganizationConfiguration;
use App\Services\WorkContext;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
// Explicit, guarded application of the owner's approved local synthetic proposal.
require dirname(__DIR__).'/vendor/autoload.php';
$root = dirname(__DIR__);
$apply = in_array('--apply-authorized', $argv, true);
$proposalFile = $root.'/docs/proofs/pilot-access-proposal.json';
$proposal = json_decode(file_get_contents($proposalFile), true, 64, JSON_THROW_ON_ERROR);
$tested = json_decode(file_get_contents($root.'/docs/proofs/pilot-access-proposal-dry-run.json'), true, 64, JSON_THROW_ON_ERROR);
$proposalHash = hash_file('sha256', $proposalFile);
if (strtolower($tested['proposal_sha256']) !== $proposalHash || $tested['sqlite']['result'] !== 'passed' || $tested['postgres']['result'] !== 'passed'
|| $proposal['environment'] !== 'http://127.0.0.1:8005' || $proposal['scope'] !== 'synthetic_local_pilot_only' || $proposal['status'] !== 'pending_owner_authorization') {
throw new LogicException('A proposta não coincide com a simulação conferida.');
}
$authorization = json_decode(file_get_contents($root.'/docs/proofs/pilot-access-authorization.json'), true, 32, JSON_THROW_ON_ERROR);
if ($authorization['proposal_sha256'] !== $proposalHash || $authorization['owner_reply'] !== 'autorizo' || $authorization['environment'] !== $proposal['environment']) {
throw new LogicException('Autorização específica da proposta obrigatória.');
}
$settings = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/payables-demo-compras.json'), true, 32, JSON_THROW_ON_ERROR);
if (($settings['APP_ENV'] ?? '') !== 'payable-testing' || ($settings['DB_HOST'] ?? '') !== '127.0.0.1'
|| ! preg_match('/^compras_receipts_test_([a-f0-9]{12})$/D', $settings['DB_DATABASE'] ?? '', $match)
|| ($settings['DB_USERNAME'] ?? '') !== 'payable_qa_'.$match[1] || realpath($settings['HOST_ROOT'] ?? '') !== realpath($root)) {
throw new LogicException('Destino limitado à avaliação local SGC existente.');
}
$app = require $root.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$owner = DB::connection('pgsql')->getConfig();
unset($owner['name']);
if ($owner['host'] !== '127.0.0.1') {
throw new LogicException('Controlo de restauro exclusivamente local.');
}
$qa = [...$owner, 'url' => null, 'host' => $settings['DB_HOST'], 'port' => $settings['DB_PORT'] ?? '5432', 'database' => $settings['DB_DATABASE'], 'username' => $settings['DB_USERNAME'], 'password' => $settings['DB_PASSWORD']];
config(['database.connections.pilot_apply' => $qa, 'database.connections.pilot_owner' => [...$owner, 'database' => 'postgres', 'url' => null], 'app.env' => 'payable-testing', 'work_context.composition' => 'sgc']);
$app->detectEnvironment(fn () => 'payable-testing');
DB::setDefaultConnection('pilot_apply');
$db = DB::connection();
$identity = $db->selectOne('select current_database() as db, current_user as role');
if ($identity->db !== $settings['DB_DATABASE'] || $identity->role !== $settings['DB_USERNAME']) {
throw new LogicException('Identidade do destino inesperada.');
}
$request = Request::create($proposal['environment'].'/administracao/parametrizacao', 'GET', server: ['REMOTE_ADDR' => '127.0.0.1']);
$app->instance('request', $request);
$request->setUserResolver(fn ($guard = null) => auth()->guard($guard)->user());
function pilotFingerprint($db, array $tables, bool $tenantProjection = false): array
{
$hashes = [];
foreach ($tables as $table) {
$quoted = '"'.str_replace('"', '""', $table).'"';
$projection = $tenantProjection && $table === 'tenants' ? "(to_jsonb(t) - ARRAY['rules_provisional','work_context_policy','updated_at'])::text" : 'row_to_json(t)::text';
$rows = $db->select('select '.$projection.' as row from '.$quoted.' t order by '.$projection);
$hashes[$table] = hash('sha256', json_encode(array_column($rows, 'row'), JSON_THROW_ON_ERROR));
}
return $hashes;
}
function pilotPg(string $binary, array $arguments, array $connection): void
{
$process = new Process([$binary, ...$arguments], null, ['PGHOST' => $connection['host'], 'PGPORT' => (string) $connection['port'], 'PGUSER' => $connection['username'], 'PGPASSWORD' => $connection['password']], timeout: 120);
$process->run();
if (! $process->isSuccessful()) {
throw new RuntimeException('Backup/restauro de controlo falhou. Nenhuma activação confirmada.');
}
}
$restore = null;
$admin = DB::connection('pilot_owner');
$failure = null;
$result = null;
try {
$result = $db->transaction(function () use ($db, $proposal, $proposalHash, $apply, $qa, $owner, $admin, &$restore) {
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
$actor = User::query()->findOrFail(8);
auth()->guard('web')->setUser($actor);
app(ConflictGuard::class)->lockCompany($actor);
$configuration = app(OrganizationConfiguration::class);
$configuration->administrator($actor);
$access = app(OrganizationAccess::class);
$contexts = app(WorkContext::class);
$state = DB::table('organization_access')->where('tenant_id', $actor->tenant_id)->lockForUpdate()->first();
if (($state->active ?? false) || (int) ($state->version ?? 1) !== $proposal['source_version']
|| DB::table('organization_profiles')->where('tenant_id', $actor->tenant_id)->exists()
|| DB::table('organization_memberships')->where('tenant_id', $actor->tenant_id)->exists()) {
throw new LogicException('A origem mudou; não substituir perfis ou atribuições existentes.');
}
$company = Tenant::query()->findOrFail($actor->tenant_id);
$expectedRules = OrganizationConfiguration::rules($proposal['current_rules']);
foreach (['first_limit_minor', 'second_limit_minor', 'approval_includes_tax'] as $field) {
if ($expectedRules[$field] !== $company->$field) {
throw new LogicException('As alçadas mudaram desde a proposta.');
}
}
if ($company->rules_provisional !== $proposal['current_rules']['provisional'] || (string) $company->default_tax_rate !== $proposal['current_rules']['default_tax_rate']
|| $contexts->policy($actor)['enabled'] !== $proposal['enabled_contexts'] || User::query()->where('tenant_id', $actor->tenant_id)->count() !== count($proposal['users'])) {
throw new LogicException('A configuração ou a lista de avaliação mudou.');
}
$before = [];
foreach ($proposal['users'] as $person) {
$user = User::query()->where('tenant_id', $actor->tenant_id)->findOrFail($person['id']);
if ($user->name !== $person['name'] || $user->department_id !== $person['department_id'] || $access->permissions($user) !== $person['current_permissions']
|| $contexts->available($user) !== $person['current_contexts'] || $access->canRequest($user) !== $person['current_can_request']) {
throw new LogicException('Uma identidade ou competência mudou desde a conferência.');
}
$before[$user->id] = ['permissions' => $access->permissions($user), 'contexts' => $contexts->available($user), 'requester' => $access->canRequest($user), 'department_scoped' => $access->departmentScoped($user)];
}
if (! $apply) {
return ['result' => 'preflight_passed_no_changes', 'users' => count($before), 'source_version' => $proposal['source_version'], 'proposal_sha256' => $proposalHash];
}
$tables = array_map(fn ($row) => $row->tablename, $db->select("select tablename from pg_tables where schemaname='public' order by tablename"));
$baseline = pilotFingerprint($db, $tables);
$protected = array_values(array_diff($tables, ['organization_access', 'organization_profiles', 'organization_memberships', 'audit_logs', 'tenants']));
$tenantBaseline = pilotFingerprint($db, ['tenants'], true);
$directory = 'C:/laragon/data/sgf-tempo/backups/compras-pilot-access-'.gmdate('Ymd-His').'-'.bin2hex(random_bytes(3));
if (! mkdir($directory, 0700, true)) {
throw new RuntimeException('Não foi possível guardar a cópia de recuperação.');
}
$dump = $directory.'/before.dump';
$snapshot = $db->selectOne('select pg_export_snapshot() as snapshot')->snapshot;
$bin = 'C:/Program Files/PostgreSQL/17/bin/';
pilotPg($bin.'pg_dump.exe', ['--format=custom', '--snapshot='.$snapshot, '--file='.$dump, '--dbname='.$qa['database']], $qa);
$restoreName = 'compras_pilot_restore_'.bin2hex(random_bytes(6));
$admin->statement('CREATE DATABASE "'.$restoreName.'"');
$restore = $restoreName;
pilotPg($bin.'pg_restore.exe', ['--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$restore, $dump], $owner);
config(['database.connections.pilot_restore' => [...$owner, 'database' => $restore, 'url' => null]]);
if (pilotFingerprint(DB::connection('pilot_restore'), $tables) !== $baseline) {
throw new RuntimeException('O restauro não coincide com a origem. Nenhuma atribuição aplicada.');
}
$reason = 'Piloto sintético SGC8005 autorizado pelo proprietário neste chat: resposta «autorizo» à proposta concreta de 02/11/2026.';
$version = $proposal['source_version'];
$profiles = [];
foreach ($proposal['profiles'] as $profile) {
$configuration->execute($actor, 'profile', ['version' => $version++, 'name' => $profile['name'], 'permissions' => $profile['permissions'], 'work_contexts' => $profile['work_contexts']]);
$profiles[$profile['key']] = DB::table('organization_profiles')->where('tenant_id', $actor->tenant_id)->where('name', $profile['name'])->sole()->id;
}
foreach ($proposal['users'] as $person) {
$configuration->execute($actor, 'assign', ['version' => $version++, 'user_id' => $person['id'], 'profile_ids' => array_map(fn ($key) => $profiles[$key], $person['profile_keys']), 'maintenance' => false, 'expires_at' => null, 'reason' => $reason]);
}
$rules = $proposal['proposed_rules'];
$configuration->execute($actor, 'rules', ['version' => $version++, 'first_limit' => $rules['first_limit'], 'second_limit' => $rules['second_limit'], 'includes_tax' => $rules['includes_tax'], 'reason' => $reason]);
$configuration->execute($actor, 'work_contexts', ['version' => $version++, 'enabled' => $proposal['enabled_contexts'], 'reason' => $reason]);
$preview = app(OrganizationActivationPreview::class)->build($actor);
$administrators = $configuration->permanentAdministrators($actor->tenant_id);
sort($administrators);
if (! $preview['can_activate'] || $preview['losing_access'] !== 0 || $administrators !== $proposal['permanent_administrator_source_ids']) {
throw new RuntimeException('A conferência anterior à activação não coincide com a proposta.');
}
$configuration->execute($actor, 'activate', ['version' => $version, 'reason' => $reason]);
foreach ($proposal['users'] as $person) {
$user = User::query()->findOrFail($person['id']);
$permissions = $access->permissions($user);
$expected = $person['planned_permissions'];
sort($permissions);
sort($expected);
if ($permissions !== $expected || $contexts->available($user) !== $before[$user->id]['contexts']
|| $access->canRequest($user) !== $before[$user->id]['requester'] || $access->departmentScoped($user) !== $before[$user->id]['department_scoped']) {
throw new RuntimeException('O acesso efectivo diverge da distribuição autorizada.');
}
}
if (pilotFingerprint($db, $protected) !== array_intersect_key($baseline, array_flip($protected)) || pilotFingerprint($db, ['tenants'], true) !== $tenantBaseline) {
throw new RuntimeException('Foi detectada alteração fora do âmbito autorizado; transacção cancelada.');
}
return ['result' => 'applied', 'checked_at' => gmdate('c'), 'environment' => $proposal['environment'], 'proposal_sha256' => $proposalHash,
'source_version' => $proposal['source_version'], 'active_version' => $version + 1, 'users_preserved' => count($before), 'profiles_created' => count($profiles),
'memberships_created' => DB::table('organization_memberships')->where('tenant_id', $actor->tenant_id)->count(), 'administrators_preserved' => $proposal['permanent_administrator_source_ids'],
'backup_file' => $dump, 'backup_sha256' => hash_file('sha256', $dump), 'restore_verified_tables' => count($tables), 'protected_tables_preserved' => count($protected),
'tenant_other_fields_preserved' => true, 'business_data_preserved' => true, 'payables_specific_grants_preserved' => true, 'new_accounts' => false,
'new_stock_publications_or_policies' => false, 'business_decisions_executed' => false, 'sgf_installation_changed' => false, 'atomic_transaction' => true];
});
} catch (Throwable $error) {
$failure = $error;
fwrite(STDERR, 'Operação não concluída ('.get_class($error).'). '.$error->getMessage().PHP_EOL);
} finally {
if ($restore !== null && preg_match('/^compras_pilot_restore_[a-f0-9]{12}$/D', $restore) && $restore !== $qa['database']) {
DB::purge('pilot_restore');
$admin->select('select pg_terminate_backend(pid) from pg_stat_activity where datname = ? and pid <> pg_backend_pid()', [$restore]);
$admin->statement('DROP DATABASE "'.$restore.'"');
}
}
if ($failure !== null) {
exit(1);
}
if ($apply) {
file_put_contents($root.'/docs/proofs/pilot-access-activation.json', json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
}
echo json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL;
+6
View File
@@ -17,6 +17,12 @@ await new Function('fs', 'assert', 'return (async()=>{' + common + `
assert.equal(warehouseDrafts.page.props.canPrepare,false);
const availability = await viewer.request('/stock/disponibilidade');
assert.equal(availability.status,200);
const replenishment = await viewer.request('/stock/reposicao');
assert.equal(replenishment.status,200);
assert.equal(replenishment.page.component,'stock/replenishment');
assert.equal(replenishment.page.props.ready,true);
assert.equal(replenishment.page.props.canPrepare,false);
assert.equal(replenishment.page.props.canReview,false);
const notices = await viewer.request('/requisicoes/avisos');
assert.equal(notices.status,200);
assert.equal(notices.page.component,'workflow/notices');
+36
View File
@@ -0,0 +1,36 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const routes = [];
for (const role of ['procurement', 'department_head', 'finance']) {
const viewer = client();
await viewer.enter(role);
const dashboard = await viewer.request('/dashboard');
const context = dashboard.page?.props.workContext ?? dashboard.page?.props.context;
if (context?.enforced) {
assert.ok(context.available?.includes('procurement.internal'));
assert.equal((await viewer.request('/contexto', 'POST', { area: 'procurement', option: 'procurement.internal' })).status, 302);
}
const response = await viewer.request('/requisicoes/tarefas?module=purchases');
assert.equal(response.status, 200);
assert.equal(response.page.props.filters.module, 'purchases');
const rows = response.page.props.tasks.data;
assert.ok(rows.every(row => row.module === 'purchases'));
const awards = rows.filter(row => ['award_approve', 'order_issue_review'].includes(row.task));
for (const task of awards) {
const detail = await viewer.request(task.href);
assert.equal(detail.status, 200);
assert.equal(detail.page.props[task.task === 'award_approve' ? 'canDecide' : 'canIssue'], true);
}
routes.push({ role, total_purchase_tasks: response.page.props.tasks.total, shown_award_tasks: awards.length, result: 'passed' });
}
await fs.writeFile('docs/proofs/award-task-integration-http.json', JSON.stringify({
checked_at: new Date().toISOString(), environment: base, routes, existing_accounts_used: true,
business_commands: false, accounts_or_grants_changed: false, visual_review: false,
note: 'Only current first-page tasks are followed; disposable tests cover pending approval and issuance.'
}, null, 2));
console.log('Encaminhamento de adjudicacoes conferido com o ambito actual dos perfis existentes.');
` + '})()')(fs, assert);
+2
View File
@@ -62,6 +62,8 @@ try {
fwrite(STDOUT, "Compras — ambiente de desenvolvimento e avaliação\n$entry\nEscolha «Entrar como proprietário» ou um perfil de avaliação.\nMesma base persistente; sem migrações, seeds ou redefinição de contas.\n");
if (! in_array('--check', $argv, true)) {
$children['reposição'] = new Process([PHP_BINARY, 'scripts/replenishment-evaluation.php', '--daemon'], $root, timeout: null);
$children['reposição']->start();
$hot = is_file($root.'/public/hot') ? trim(file_get_contents($root.'/public/hot')) : '';
$host = parse_url($hot, PHP_URL_HOST);
$viteReady = in_array($host, ['127.0.0.1', 'localhost', '[::1]'], true)
+39
View File
@@ -0,0 +1,39 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const routes = [];
for (const role of ['admin', 'finance']) {
const viewer = client();
await viewer.enter(role);
const dashboard = await viewer.request('/dashboard');
const context = dashboard.page?.props.workContext ?? dashboard.page?.props.context;
if (context?.enforced) {
assert.ok(context.available?.includes('procurement.internal'));
assert.equal((await viewer.request('/contexto', 'POST', { area: 'procurement', option: 'procurement.internal' })).status, 302);
}
for (const path of ['/facturas', '/pagamentos', '/requisicoes/tarefas?module=finance']) {
const response = await viewer.request(path);
assert.equal(response.status, 200, role + ' ' + path);
const props = response.page.props;
if (path.startsWith('/requisicoes')) {
assert.equal(props.modules.finance, 'Finanças');
assert.equal(props.filters.module, 'finance');
routes.push({ role, path, total: props.tasks.total, result: 'passed' });
} else {
const data = props.invoices?.data ?? props.payments?.data;
assert.ok(Array.isArray(data));
assert.equal(path === '/facturas' ? props.focusInvoiceId : props.focusPaymentId, null);
routes.push({ role, path, shown: data.length, result: 'passed' });
}
}
}
await fs.writeFile('docs/proofs/finance-integration-http.json', JSON.stringify({
checked_at: new Date().toISOString(), environment: base, routes, existing_accounts_used: true,
business_commands: false, accounts_or_grants_changed: false, sgf_installation_changed: false, visual_review: false,
note: 'Payment consultation retains its existing access audit; no financial decision is executed.'
}, null, 2));
console.log('Facturas, pagamentos e tarefas financeiras conferidos com as contas existentes.');
` + '})()')(fs, assert);
+41
View File
@@ -0,0 +1,41 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const routes = [];
for (const role of ['procurement', 'finance', 'department_head']) {
const viewer = client();
await viewer.enter(role);
assert.equal((await viewer.request('/contexto', 'POST', { area: 'procurement', option: 'procurement.internal' })).status, 302);
const tasks = await viewer.request('/requisicoes/tarefas?module=purchases');
assert.equal(tasks.status, 200);
const formal = tasks.page.props.tasks.data.filter(task => ['qualification_complete', 'admission_review', 'recommendation_review'].includes(task.task));
for (const task of formal) {
const detail = await viewer.request(task.href);
assert.equal(detail.status, 200);
if (task.task === 'qualification_complete') assert.equal(detail.page.props.canAssess, true);
else assert.equal(detail.page.props.history.data[0]?.canReview, true);
}
const index = await viewer.request('/cotacoes');
assert.equal(index.status, 200);
const request = index.page.props.requisitions.data[0];
const components = [];
if (request) {
for (const [path, component] of [['qualificacao', 'purchases/qualification'], ['admissao', 'purchases/admission'], ['recomendacao', 'purchases/recommendation']]) {
const response = await viewer.request('/requisicoes/' + request.id + '/' + path);
assert.equal(response.status, 200);
assert.equal(response.page.component, component);
components.push({ component, result: 'passed' });
}
}
routes.push({ role, formal_tasks_shown: formal.length, task_links_followed: formal.length, components });
}
await fs.writeFile('docs/proofs/formal-task-integration-http.json', JSON.stringify({
checked_at: new Date().toISOString(), environment: base, result: 'passed', routes,
business_commands: false, accounts_or_grants_changed: false, sgf_installation_changed: false,
visual_review: false, coverage: 'Existing current scope and first-page tasks only; pending decisions tested in disposable databases.'
}, null, 2));
console.log('Analise documental, admissao, recomendacao e tarefas conferidas nos ambitos existentes.');
` + '})()')(fs, assert);
@@ -0,0 +1,30 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const viewer = client();
await viewer.enter('admin');
const dashboard = await viewer.request('/dashboard');
if (dashboard.page?.props.context?.enforced || dashboard.page?.props.workContext?.enforced) {
await viewer.request('/contexto', 'POST', { area: 'procurement', option: 'procurement.internal' });
}
const index = await viewer.request('/stock/requisicoes');
assert.equal(index.status, 200);
let checkedDetails = 0;
for (const record of (index.page?.props.records?.data ?? []).slice(0, 3)) {
const detail = await viewer.request('/stock/requisicoes/' + record.id);
assert.equal(detail.status, 200);
assert.equal(typeof detail.page.props.supplyTrace.ready, 'boolean');
assert.ok(Array.isArray(detail.page.props.supplyTrace.groups));
checkedDetails++;
}
await fs.writeFile('docs/proofs/internal-supply-integration-http.json', JSON.stringify({
checked_at: new Date().toISOString(), environment: base, result: 'passed', internal_sgc_only: true,
warehouse_index: true, existing_request_details_checked: checkedDetails,
synthetic_journeys_in_disposable_tests: true, persistent_business_commands: false,
accounts_or_grants_changed: false, sgf_installation_changed: false, visual_review: false
}, null, 2));
console.log('Consulta da integração interna conferida; detalhes existentes verificados: ' + checkedDetails);
` + '})()')(fs, assert);
@@ -0,0 +1,34 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const viewer = client();
await viewer.enter('admin');
const response = await viewer.request('/administracao/parametrizacao');
assert.equal(response.status, 200);
const props = response.page.props;
const preview = props.activationPreview;
assert.equal(preview.ready, true);
assert.equal(preview.version, props.version);
assert.equal(preview.active, props.active);
assert.equal(preview.users.length, props.users.length);
const proof = {
checked_at: new Date().toISOString(), environment: base, result: 'passed', version: props.version,
active: props.active, users: preview.users.length, losing_access: preview.losing_access,
permanent_administrators: preview.permanent_administrators, can_activate: preview.can_activate,
provisional_rules: preview.provisional_rules, existing_account_used: true, business_commands: false,
accounts_or_grants_changed: false, sgf_installation_changed: false, visual_review: false
};
await fs.writeFile('docs/proofs/organization-activation-preview-http.json', JSON.stringify(proof, null, 2));
const snapshot = {
captured_at: proof.checked_at, environment: base, version: props.version, active: props.active,
company_rules: props.companyRules, enabled_contexts: props.enabledWorkContexts,
users: preview.users.map(person => ({ ...person, department_id: props.users.find(user => user.id === person.id)?.department_id ?? null })),
profiles: props.profiles, memberships: props.memberships, departments: props.departments, cost_centers: props.costCenters
};
await fs.writeFile('storage/app/pilot-access-source.json', JSON.stringify(snapshot, null, 2));
console.log(JSON.stringify({ ...proof, current_rules: props.companyRules,
registered: snapshot.users.map(person => ({ id: person.id, name: person.name, department_id: person.department_id, functions: person.current_permissions, contexts: person.current_contexts })) }, null, 2));
` + '})()')(fs, assert);
+86
View File
@@ -0,0 +1,86 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const proposal = JSON.parse(await fs.readFile('docs/proofs/pilot-access-proposal.json', 'utf8'));
const applied = JSON.parse(await fs.readFile('docs/proofs/pilot-access-activation.json', 'utf8'));
assert.equal(applied.result, 'applied');
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'proposal', 'applied', 'return (async()=>{' + common + `
const viewer = client();
await viewer.enter('admin');
const parameters = await viewer.request('/administracao/parametrizacao');
assert.equal(parameters.status, 200);
const p = parameters.page.props;
assert.equal(p.active, true);
assert.equal(p.version, applied.active_version);
assert.equal(p.profiles.length, 11);
assert.equal(p.memberships.length, 17);
assert.equal(p.users.length, 12);
assert.equal(p.activationPreview.permanent_administrators, 2);
assert.equal(p.activationPreview.losing_access, 0);
for (const person of proposal.users) {
const actual = p.activationPreview.users.find(user => user.id === person.id);
assert.equal(actual.name, person.name);
assert.deepEqual([...actual.current_permissions].sort(), [...person.planned_permissions].sort());
assert.deepEqual(actual.current_contexts, person.current_contexts);
assert.equal(actual.current_can_request, person.current_can_request);
assert.equal(p.users.find(user => user.id === person.id).department_id, person.department_id);
}
assert.equal(p.companyRules.first_limit, proposal.proposed_rules.first_limit);
assert.equal(p.companyRules.second_limit, proposal.proposed_rules.second_limit);
assert.equal(p.companyRules.includes_tax, proposal.proposed_rules.includes_tax);
assert.equal(p.companyRules.default_tax_rate, proposal.current_rules.default_tax_rate);
assert.equal(p.companyRules.provisional, false);
async function selectInternal(client, area = 'procurement') {
const response = await client.request('/contexto', 'POST', { area, option: area + '.internal' });
assert.equal(response.status, 302);
}
await selectInternal(viewer);
const readiness = await viewer.request('/stock/piloto');
assert.equal(readiness.status, 200);
const report = readiness.page.props.report;
for (const [group, ids] of [['prepare', [9]], ['review', [10]], ['deliver', [13]], ['supply_prepare', [11]], ['supply_review', [12]]]) {
assert.deepEqual(report.participants[group].map(user => user.id).sort((a, b) => a - b), ids);
}
assert.equal(report.checks.find(check => check.key === 'participants').ok, true);
assert.equal(report.participants.purchase_requester.length, 12);
const roles = [];
for (const [role, path, capability] of [
['requester', '/stock/requisicoes', 'canPrepare'],
['department_head', '/stock/requisicoes?view=review', null],
['warehouse', '/stock/requisicoes?view=delivery', null],
['procurement', '/stock/reposicao', 'canPrepare'],
['finance', '/stock/reposicao', 'canReview']
]) {
const person = client();
await person.enter(role);
await selectInternal(person);
const response = await person.request(path);
assert.equal(response.status, 200, role + ' ' + path);
if (capability) assert.equal(response.page.props[capability], true);
if (role === 'procurement') assert.equal(response.page.props.canReview, false);
if (role === 'finance') assert.equal(response.page.props.canPrepare, false);
if (['warehouse', 'department_head'].includes(role)) assert.equal(response.page.props.canPrepare, false);
assert.equal((await person.request('/administracao/parametrizacao')).status, 403);
const tasks = await person.request('/requisicoes/tarefas');
assert.equal(tasks.status, 200);
roles.push({ role, route: path, result: 'passed', tasks: tasks.page.props.tasks.total });
}
await selectInternal(viewer, 'people');
assert.equal((await viewer.request('/pessoas/tarefas')).status, 200);
const proof = {
checked_at: new Date().toISOString(), environment: base, result: 'passed', active: true,
version: p.version, users: 12, profiles: 11, memberships: 17, permanent_administrators: 2,
original_access_and_contexts_preserved: true, first_limit: p.companyRules.first_limit,
second_limit: p.companyRules.second_limit, includes_tax: p.companyRules.includes_tax, provisional_rules: false,
participants_separated: true, roles, readiness: {
warehouse_scenario_ready: report.warehouse_scenario_ready,
replenishment_scenario_ready: report.replenishment_scenario_ready,
client_configuration_complete: report.client_configuration_complete,
checks: report.checks.map(check => ({ key: check.key, ok: check.ok, detail: check.detail }))
}, business_commands_executed: false, new_accounts: false, sgf_changes: false, visual_review: false
};
await fs.writeFile('docs/proofs/pilot-access-activation-http.json', JSON.stringify(proof, null, 2));
console.log(JSON.stringify(proof, null, 2));
` + '})()')(fs, assert, proposal, applied);
+12
View File
@@ -0,0 +1,12 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const viewer=client();await viewer.enter('admin');const dashboard=await viewer.request('/dashboard');
if(dashboard.page?.props.context?.enforced)await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'});
const response=await viewer.request('/stock/piloto.csv','GET',undefined,false);
assert.equal(response.status,200);assert.ok(response.text.includes('Conferido em'));assert.ok(response.text.includes('SGC autónomo'));assert.ok(response.text.includes('A preparar'));assert.ok(!response.text.includes('Solicitante — avaliação local'));
await fs.writeFile('docs/proofs/pilot-export-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,csv:'passed',dated_configuration_checks:true,account_register_exported:false,business_commands:false,accounts_or_grants_changed:false,visual_print_review:false},null,2));
console.log('Exportação administrativa do diagnóstico conferida em HTTP.');
`+'})()')(fs,assert);
+66
View File
@@ -0,0 +1,66 @@
<?php
// Produces a reviewable proposal only. No application bootstrap, database or network.
$root = dirname(__DIR__);
$path = $root.'/storage/app/pilot-access-source.json';
$raw = file_get_contents($path);
$source = json_decode($raw, true, 64, JSON_THROW_ON_ERROR);
if ($source['environment'] !== 'http://127.0.0.1:8005' || $source['active'] !== false || $source['profiles'] !== [] || $source['memberships'] !== []) {
throw new LogicException('Rever a origem: proposta limitada à avaliação sintética ainda sem perfis.');
}
$additions = [
9 => ['expected_role' => 'requester', 'name' => 'Requisitar ao armazém', 'permission' => 'warehouse_request_prepare'],
10 => ['expected_role' => 'department_head', 'name' => 'Autorizar requisições internas', 'permission' => 'warehouse_request_review'],
13 => ['expected_role' => 'warehouse', 'name' => 'Entregar requisições internas', 'permission' => 'warehouse_request_deliver'],
11 => ['expected_role' => 'procurement', 'name' => 'Preparar reposição', 'permission' => 'supply_prepare'],
12 => ['expected_role' => 'finance', 'name' => 'Conferir reposição', 'permission' => 'supply_review'],
];
$profiles = [];
$people = [];
$admins = [];
foreach ($source['users'] as $person) {
if (count($person['current_permissions']) !== 1 || ! is_array($person['current_contexts']) || ! $person['current_can_request']) {
throw new LogicException('A origem deixou de corresponder às funções de desenvolvimento esperadas.');
}
$role = $person['current_permissions'][0];
$key = 'base.'.$role;
$base = ['key' => $key, 'name' => 'Piloto sintético — base '.$role,
'permissions' => array_values(array_unique([$role, 'requester'])), 'work_contexts' => $person['current_contexts']];
if (isset($profiles[$key]) && $profiles[$key] !== $base) {
throw new LogicException('Contextos diferentes exigem proposta por pessoa, sem expansão implícita.');
}
$profiles[$key] = $base;
$keys = [$key];
$planned = $base['permissions'];
if ($role === 'admin') {
$admins[] = $person['id'];
}
if (isset($additions[$person['id']])) {
$extra = $additions[$person['id']];
if ($role !== $extra['expected_role'] || ! str_contains($person['name'], 'avaliação local')) {
throw new LogicException('Uma identidade proposta mudou. Confirmar os intervenientes antes de prosseguir.');
}
$extraKey = 'task.'.$person['id'];
$profiles[$extraKey] = ['key' => $extraKey, 'name' => 'Piloto sintético — '.$extra['name'], 'permissions' => [$extra['permission']], 'work_contexts' => ['procurement.internal']];
$keys[] = $extraKey;
$planned[] = $extra['permission'];
}
$people[] = ['id' => $person['id'], 'name' => $person['name'], 'department_id' => $person['department_id'],
'current_permissions' => $person['current_permissions'], 'current_contexts' => $person['current_contexts'],
'current_can_request' => $person['current_can_request'], 'profile_keys' => $keys,
'planned_permissions' => $planned, 'added_task_permissions' => isset($additions[$person['id']]) ? [$additions[$person['id']]['permission']] : []];
}
sort($admins);
if (count($people) !== 12 || $admins !== [6, 8] || count(array_intersect(array_column($people, 'id'), array_keys($additions))) !== 5) {
throw new LogicException('A lista de avaliação mudou. Produzir nova proposta revista.');
}
$proposal = ['status' => 'pending_owner_authorization', 'scope' => 'synthetic_local_pilot_only', 'environment' => $source['environment'],
'prepared_at' => gmdate('c'), 'source_captured_at' => $source['captured_at'], 'source_sha256' => hash('sha256', $raw), 'source_version' => $source['version'],
'current_rules' => $source['company_rules'], 'proposed_rules' => [...$source['company_rules'], 'provisional' => false],
'enabled_contexts' => $source['enabled_contexts'], 'profiles' => array_values($profiles), 'users' => $people, 'permanent_administrator_source_ids' => $admins,
'effects' => ['activate_saved_profiles' => true, 'preserve_existing_permissions_and_contexts' => true, 'preserve_legacy_request_preparation' => true,
'make_existing_approval_limits_explicit_for_synthetic_pilot' => true, 'new_accounts' => false, 'change_existing_process_snapshots' => false,
'change_payables_specific_grants' => false, 'publish_stock_or_set_minimum_quantities' => false, 'change_sgf' => false],
'departments' => $source['departments'], 'cost_centers' => $source['cost_centers']];
file_put_contents($root.'/docs/proofs/pilot-access-proposal.json', json_encode($proposal, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
echo json_encode(['result' => 'proposal_prepared_not_applied', 'users' => count($people), 'profiles' => count($profiles), 'new_task_assignments' => count($additions), 'permanent_administrators' => $admins], JSON_THROW_ON_ERROR).PHP_EOL;
+44 -1
View File
@@ -3,15 +3,19 @@
use Tempo\Purchasing\Delivery\AccessOverlay;
use Tempo\Purchasing\Delivery\BootOverlay;
use Tempo\Purchasing\Delivery\BudgetOverlay;
use Tempo\Purchasing\Delivery\AdminUiOverlay;
require dirname(__DIR__).'/integrations/sgf/access/AccessOverlay.php';
require dirname(__DIR__).'/integrations/sgf/operational/BudgetOverlay.php';
require dirname(__DIR__).'/integrations/sgf/operational/BootOverlay.php';
require dirname(__DIR__).'/integrations/sgf/operational/AdminUiOverlay.php';
$host = realpath($argv[1] ?? dirname(__DIR__).'/../sgf-tempo');
if (! $host) {
throw new RuntimeException('Host inexistente.');
}
$files = AccessOverlay::prepare($host) + BudgetOverlay::prepare($host) + BootOverlay::prepare($host);
$frontend = AdminUiOverlay::prepare($host, $files);
$files = $frontend['files'];
$root = dirname(__DIR__);
$support = [];
foreach (['integrations/sgf/operational', 'integrations/sgf/access', 'integrations/sgf/internal-requisitions', 'packages/purchasing/src'] as $directory) {
@@ -22,7 +26,9 @@ foreach (['integrations/sgf/operational', 'integrations/sgf/access', 'integratio
}
}
ksort($support);
$support += $frontend['assets'];
$support['scripts/sgf-internal-install.php'] = file_get_contents($root.'/scripts/sgf-internal-install.php');
$support['scripts/sgf-code-deploy.php'] = file_get_contents($root.'/scripts/sgf-code-deploy.php');
$support['bootstrap.php'] = file_get_contents($root.'/integrations/sgf/operational/entry.php');
$hash = hash('sha256', json_encode([$files, $support], JSON_THROW_ON_ERROR));
$directory = $root.'/storage/app/private/candidates/OPERATIONAL-CANDIDATE-'.substr($hash, 0, 16);
@@ -44,4 +50,41 @@ foreach ($support as $path => $contents) {
$manifest['support_files'][$path] = hash('sha256', $contents);
}
file_put_contents($directory.'/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode(['directory' => $directory, 'installed' => false], JSON_THROW_ON_ERROR)."\n";
$archives = $root.'/storage/app/private/candidate-archives';
if (! is_dir($archives)) {
mkdir($archives, 0700, true);
}
$archive = $archives.'/'.basename($directory).'.zip';
$creating = ! is_file($archive);
$archivePath = $creating ? $archive.'.'.bin2hex(random_bytes(6)).'.tmp' : $archive;
$zip = new ZipArchive;
if ($zip->open($archivePath, $creating ? ZipArchive::CREATE | ZipArchive::EXCL : 0) !== true) {
throw new RuntimeException('Arquivo de candidata inválido.');
}
if ($creating) {
foreach ([...array_keys($manifest['support_files']), 'manifest.json'] as $file) {
$zip->addFile($directory.'/'.$file, $file);
}
if (! $zip->close()) {
throw new RuntimeException('Falha ao preparar arquivo.');
}
if ($zip->open($archivePath) !== true) {
throw new RuntimeException('Arquivo incompleto.');
}
}
if ($zip->numFiles !== count($manifest['support_files']) + 1) {
throw new RuntimeException('Conteúdo extra/ausente no arquivo.');
}
foreach ($manifest['support_files'] + ['manifest.json' => hash_file('sha256', $directory.'/manifest.json')] as $file => $expected) {
$size = filesize($directory.'/'.$file);
$entry = $zip->statName($file);
$body = $zip->getFromName($file, $size + 1);
if (! $entry || $entry['size'] !== $size || $body === false || ! hash_equals($expected, hash('sha256', $body))) {
throw new RuntimeException('Integridade do arquivo imutável divergente.');
}
}
$zip->close();
if ($creating && ! rename($archivePath, $archive)) {
throw new RuntimeException('Falha ao concluir arquivo.');
}
echo json_encode(['directory' => $directory, 'archive' => ['file' => $archive, 'sha256' => hash_file('sha256', $archive), 'manifest_sha256' => hash_file('sha256', $directory.'/manifest.json')], 'installed' => false], JSON_THROW_ON_ERROR)."\n";
@@ -0,0 +1,39 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const routes = [];
for (const role of ['procurement', 'finance', 'department_head']) {
const viewer = client();
await viewer.enter(role);
const dashboard = await viewer.request('/dashboard');
const context = dashboard.page?.props.workContext ?? dashboard.page?.props.context;
if (context?.enforced) {
assert.ok(context.available?.includes('procurement.internal'));
assert.equal((await viewer.request('/contexto', 'POST', { area: 'procurement', option: 'procurement.internal' })).status, 302);
}
const response = await viewer.request('/requisicoes/tarefas?module=purchases');
assert.equal(response.status, 200);
const tasks = response.page.props.tasks.data.filter(task => ['quotation_prepare', 'quotation_evaluate', 'quotation_check', 'comparison_review'].includes(task.task));
for (const task of tasks) {
const detail = await viewer.request(task.href);
assert.equal(detail.status, 200);
const props = detail.page.props;
if (task.task === 'quotation_prepare') {
assert.equal(props.canCreate, true);
assert.equal(props.prepare, true);
} else if (task.task === 'quotation_evaluate') {
assert.ok(props.quotations.find(quote => quote.id === props.focusQuotationId)?.canEvaluate);
}
}
routes.push({ role, purchase_tasks: response.page.props.tasks.total, shown_quotation_tasks: tasks.length, followed: tasks.length, result: 'passed' });
}
await fs.writeFile('docs/proofs/quotation-task-integration-http.json', JSON.stringify({
checked_at: new Date().toISOString(), environment: base, routes, existing_accounts_used: true,
business_commands: false, accounts_or_grants_changed: false, sgf_installation_changed: false,
visual_review: false, coverage: 'Current first-page tasks only; complete pending journeys covered by disposable tests.'
}, null, 2));
console.log('Consulta ao mercado e ligacoes das tarefas conferidas com os perfis existentes.');
` + '})()')(fs, assert);
+53
View File
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
use Symfony\Component\Process\Process;
// Resume only the existing SGC evaluation. Never creates a database, user or grant.
require dirname(__DIR__).'/vendor/autoload.php';
$root = dirname(__DIR__);
$settings = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/payables-demo-compras.json'), true, 32, JSON_THROW_ON_ERROR);
if (($settings['APP_ENV'] ?? '') !== 'payable-testing' || ($settings['DB_HOST'] ?? '') !== '127.0.0.1' || ! preg_match('/^compras_receipts_test_[a-f0-9]{12}$/D', $settings['DB_DATABASE'] ?? '') || realpath($settings['HOST_ROOT'] ?? '') !== realpath($root)) {
throw new LogicException('Configuração da avaliação SGC existente obrigatória.');
}
foreach ($settings as $key => $value) {
putenv($key.'='.$value);
$_ENV[$key] = $_SERVER[$key] = $value;
}
// This helper deliberately excludes the SGF composition and its production connection.
putenv('SGC_PRODUCT_COMPOSITION=sgc');
$_ENV['SGC_PRODUCT_COMPOSITION'] = $_SERVER['SGC_PRODUCT_COMPOSITION'] = 'sgc';
$daemon = in_array('--daemon', $argv, true);
$lock = fopen($root.'/storage/framework/replenishment-evaluation.lock', 'c');
if ($lock === false) {
throw new RuntimeException('Não foi possível abrir o controlo da verificação periódica.');
}
if (! flock($lock, LOCK_EX | LOCK_NB)) {
fwrite(STDOUT, "Verificação periódica já em execução.\n");
if (! $daemon) {
exit(0);
}
// Standby until the running helper exits; never run two local schedulers together.
if (! flock($lock, LOCK_EX)) {
throw new RuntimeException('Não foi possível acompanhar o verificador existente.');
}
}
do {
$started = gmdate('c');
$process = new Process([PHP_BINARY, 'artisan', 'sgc:replenishment-tick'], $root, timeout: 55);
$process->run();
// Deliberately no raw exception/configuration content in the public status evidence.
$status = ['started_at' => $started, 'finished_at' => gmdate('c'), 'pid' => getmypid(), 'scope' => 'SGC evaluation 8005', 'result' => $process->isSuccessful() ? 'passed' : 'retry_pending', 'exit_code' => $process->getExitCode()];
$temporary = $root.'/storage/framework/replenishment-status.tmp';
file_put_contents($temporary, json_encode($status, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
rename($temporary, $root.'/storage/framework/replenishment-status.json');
fwrite(STDOUT, json_encode($status, JSON_THROW_ON_ERROR)."\n");
if (! $daemon) {
flock($lock, LOCK_UN);
fclose($lock);
exit($process->isSuccessful() ? 0 : 1);
}
// Technical poll every minute; each work item has its own persisted next run time.
sleep(60);
} while (true);
+18
View File
@@ -0,0 +1,18 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const viewer=client(); await viewer.enter('admin');
const dashboard=await viewer.request('/dashboard');
if(dashboard.page?.props.context?.enforced)await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'});
const page=await viewer.request('/stock/reposicao?need_state=pending');
assert.equal(page.status,200); assert.equal(page.page.component,'stock/replenishment');
const p=page.page.props; assert.equal(p.ready,true); assert.equal(p.recoveryReady,true);
assert.equal(p.canPrepare,false); assert.equal(p.canReview,false);
for(const key of ['rules','needs','history','workItems']){assert.ok(Array.isArray(p[key].data));assert.equal(typeof p[key].total,'number');}
assert.equal(p.filters.need_state,'pending'); assert.equal(p.schedulerStatus.result,'passed');
assert.ok(Date.now()-Date.parse(p.schedulerStatus.finished_at)<120000);
await fs.writeFile('docs/proofs/replenishment-pilot-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,component:'stock/replenishment',recovery_installed:true,pagination:['rules','needs','history','workItems'],filter:'pending',scheduler:p.schedulerStatus,administrator_read_only:true,business_commands:false,accounts_or_grants_changed:false,visual_browser_review:false},null,2));
console.log('Reposição: paginação, filtro, recuperação e passagem recente do serviço confirmados por HTTP.');
`+'})()')(fs,assert);
+48
View File
@@ -0,0 +1,48 @@
// Build in a private source copy; never writes the installed SGF or dependencies.
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import {spawnSync} from 'node:child_process';
const host=fs.realpathSync(process.argv[2]||'');
if(fs.existsSync(path.join(host,'public/hot')))throw Error('Stop the source frontend watcher before freezing a candidate.');
const work=path.join('C:/laragon/data/sgf-tempo/run','candidate-ui-'+crypto.randomBytes(6).toString('hex'));
fs.mkdirSync(work,{recursive:true});
const replace=(text,old,next)=>{if(text.split(old).length!==2)throw Error('Frontend baseline changed: '+old.slice(0,70));return text.replace(old,next);};
fs.cpSync(path.join(host,'resources'),path.join(work,'resources'),{recursive:true});
for(const file of ['package.json','vite.config.js'])fs.copyFileSync(path.join(host,file),path.join(work,file));
fs.mkdirSync(path.join(work,'public/build'),{recursive:true});
fs.symlinkSync(path.join(host,'node_modules'),path.join(work,'node_modules'),'junction');
let app=fs.readFileSync(path.join(work,'resources/js/app.jsx'),'utf8');
app=replace(app,"const options = (unit === null ? ['company.view', 'company.edit', 'unit.create', 'access.manage'] : ['unit.view', 'unit.edit']).filter(p => can(p, unit));", "const options = (list?.catalogue?.[unit === null ? 'company' : 'unit'] ?? []).filter(p => can(p, unit));");
app=replace(app,'{labels[p]}','{list?.catalogue_labels?.[p] ?? labels[p] ?? p}');
app=replace(app,'{labels[g.permission]}','{list?.catalogue_labels?.[g.permission] ?? labels[g.permission] ?? g.permission}');
fs.writeFileSync(path.join(work,'resources/js/app.jsx'),app);
let home=fs.readFileSync(path.join(work,'resources/js/PurchasingHome.jsx'),'utf8');
home=replace(home,"const payables=unit!==null&&can('supplier_payables.view',unit);", "const payables=unit!==null&&can('supplier_payables.view',unit);\n const requisitions=unit!==null&&can('purchasing_requisitions.view',unit);");
home=replace(home,'<div className="actions">','<div className="actions">{requisitions&&<a href="/purchasing/internal-requisitions">Requisições internas</a>}{budget&&<a href="/purchasing/internal-requisitions/budget-setup">Preparação de centros e dotações</a>}');
home=replace(home,'!budget&&!payables&&','!budget&&!payables&&!requisitions&&');
fs.writeFileSync(path.join(work,'resources/js/PurchasingHome.jsx'),home);
let config=fs.readFileSync(path.join(work,'vite.config.js'),'utf8');
config=replace(config,'defineConfig({',`defineConfig({\n cacheDir: ${JSON.stringify(path.join(work,'vite-cache'))},`);
fs.writeFileSync(path.join(work,'vite.config.js'),config);
const result=spawnSync(process.execPath,[path.join(host,'node_modules/vite/bin/vite.js'),'build','--config',path.join(work,'vite.config.js'),'--configLoader','native'],{cwd:work,encoding:'utf8',windowsHide:true,maxBuffer:4*1024*1024,env:{...process.env,NODE_ENV:'production'}});
fs.writeFileSync(path.join(work,'build.log'),(result.stdout||'')+(result.stderr||''));
if(result.status!==0)throw Error('Candidate frontend build failed; private log '+path.join(work,'build.log'));
const original=JSON.parse(fs.readFileSync(path.join(work,'public/build/manifest.json'),'utf8'));
const assets={};
for(const chunk of Object.values(original))for(const file of [chunk.file,...(chunk.css||[]),...(chunk.assets||[])]){
if(!/^assets\/[A-Za-z0-9_-]+\.(js|css)$/.test(file))throw Error('Unsupported frontend asset '+file);
assets['integrations/sgf/frontend/'+file]=path.join(work,'public/build',file);
}
const manifest=JSON.parse(JSON.stringify(original));
for(const chunk of Object.values(manifest)){
chunk.file='modules/internal-requisitions/'+chunk.file;
if(chunk.css)chunk.css=chunk.css.map(p=>'modules/internal-requisitions/'+p);
if(chunk.assets)chunk.assets=chunk.assets.map(p=>'modules/internal-requisitions/'+p);
}
const manifestFile=path.join(work,'candidate-manifest.json');fs.writeFileSync(manifestFile,JSON.stringify(manifest,null,2)+'\n');
const overlays={'resources/js/app.jsx':path.join(work,'resources/js/app.jsx'),'resources/js/PurchasingHome.jsx':path.join(work,'resources/js/PurchasingHome.jsx'),'public/build/manifest.json':manifestFile};
const metadata={work,overlays,assets,dependency_install:false,source_host_modified:false};
const metadataFile=path.join(work,'candidate-ui.json');fs.writeFileSync(metadataFile,JSON.stringify(metadata,null,2));
console.log(JSON.stringify({metadata:metadataFile}));
+145
View File
@@ -0,0 +1,145 @@
<?php
require dirname(__DIR__).'/integrations/sgf/operational/CodeDeployment.php';
require dirname(__DIR__).'/integrations/sgf/access/AccessOverlay.php';
require dirname(__DIR__).'/integrations/sgf/operational/BudgetOverlay.php';
require dirname(__DIR__).'/integrations/sgf/operational/BootOverlay.php';
use Tempo\Purchasing\Delivery\CodeDeployment;
$root = dirname(__DIR__);
$host = realpath($root.'/../sgf-tempo');
$pointer = file_get_contents($host.'/packages/purchasing/current.json');
$checks = [];
$check = function (bool $ok, string $name) use (&$checks) {
if (! $ok) {
throw new RuntimeException($name);
}$checks[] = $name;
};
$deny = function (callable $work, string $text) use ($check) {
try {
$work();
throw new LogicException('Unexpected success');
} catch (RuntimeException $error) {
$check(str_contains($error->getMessage(), $text), $text);
}
};
$command = escapeshellarg(PHP_BINARY).' '.escapeshellarg($root.'/scripts/prepare-sgf-operational.php').' '.escapeshellarg($host);
exec($command, $output, $exit);
if ($exit !== 0) {
throw new RuntimeException('Prepare failed');
}
$bundle = json_decode(implode("\n", $output), true, 16, JSON_THROW_ON_ERROR)['directory'];
$manifest = json_decode(file_get_contents($bundle.'/manifest.json'), true, 32, JSON_THROW_ON_ERROR);
$hash = hash_file('sha256', $bundle.'/manifest.json');
$make = function () use ($host, $manifest) {
$copy = sys_get_temp_dir().'/sgf-code-deploy-'.bin2hex(random_bytes(6));
mkdir($copy, 0700);
foreach ($manifest['host_overlays'] as $file => $values) {
if (! is_dir(dirname($copy.'/'.$file))) {
mkdir(dirname($copy.'/'.$file), 0700, true);
}copy($host.'/'.$file, $copy.'/'.$file);
}
mkdir($copy.'/packages/purchasing', 0700, true);
copy($host.'/packages/purchasing/current.json', $copy.'/packages/purchasing/current.json');
mkdir($copy.'/bootstrap/cache', 0700, true);
mkdir($copy.'/storage/framework', 0700, true);
file_put_contents($copy.'/bootstrap/cache/config.php', "<?php return ['synthetic'=>true];");
file_put_contents($copy.'/bootstrap/cache/routes-v7.php', '<?php return [];');
file_put_contents($copy.'/bootstrap/cache/unrelated.php', '<?php // preserve unrelated cache support');
return $copy;
};
$copy = $make();
$deploy = new CodeDeployment($copy);
$deny(fn () => new CodeDeployment(''), 'Host explícito obrigatório');
$deny(fn () => $deploy->plan('', $hash), 'Bundle explícito obrigatório');
$plan = $deploy->plan($bundle, $hash);
$check(! $plan['writes'] && ! is_dir($copy.'/storage/app'), 'plan is read-only');
$deny(fn () => $deploy->plan($bundle, str_repeat('0', 64)), 'Manifesto divergente');
file_put_contents($copy.'/.env', 'APP_CONFIG_CACHE=storage/framework/custom.php');
$deny(fn () => $deploy->plan($bundle, $hash), 'Cache Laravel personalizado');
unlink($copy.'/.env');
$deny(fn () => $deploy->apply($bundle, $hash, $plan['target_sha256'], 'Synthetic operator', true), 'manutenção activa');
file_put_contents($copy.'/storage/framework/down', '{"status":503}');
$deny(fn () => $deploy->apply($bundle, $hash, $plan['target_sha256'], 'Synthetic operator', false), 'serviços de escrita parados');
$deny(fn () => $deploy->apply($bundle, $hash, str_repeat('0', 64), 'Synthetic operator', true), 'Destino de código divergente');
$beforeCache = file_get_contents($copy.'/bootstrap/cache/config.php');
$result = $deploy->apply($bundle, $hash, $plan['target_sha256'], 'Synthetic operator', true);
$id = $result['deployment_id'];
$check($result['state'] === 'applied' && ! $result['activation_performed'] && is_file($copy.'/storage/framework/down'), 'applied offline without activation');
$check(! is_file($copy.'/bootstrap/cache/config.php') && ! is_file($copy.'/bootstrap/cache/routes-v7.php') && is_file($copy.'/bootstrap/cache/unrelated.php'), 'invalidates known Laravel caches without touching unrelated files');
foreach ($manifest['host_overlays'] as $file => $values) {
$check(hash_file('sha256', $copy.'/'.$file) === $values['after_sha256'], 'applied verified overlay '.$file);
}
$check($deploy->apply($bundle, $hash, $plan['target_sha256'], 'Synthetic operator', true)['replayed'], 'apply retry does not duplicate installation');
$file = array_key_first($manifest['host_overlays']);
$applied = file_get_contents($copy.'/'.$file);
file_put_contents($copy.'/'.$file, $applied."\n// local change\n");
$deny(fn () => $deploy->rollback($id, 'Synthetic operator', true), 'Código local alterado');
$check(is_dir($copy.'/packages/internal-requisitions'), 'refused rollback preserves bundle and local code');
file_put_contents($copy.'/'.$file, $applied);
file_put_contents($copy.'/bootstrap/cache/config.php', '<?php return ["new_configuration"=>true];');
$deny(fn () => $deploy->rollback($id, 'Synthetic operator', true), 'Cache recriado');
unlink($copy.'/bootstrap/cache/config.php');
$rollback = $deploy->rollback($id, 'Synthetic operator', true);
$check($rollback['state'] === 'rolled-back' && ! $rollback['data_schema_changed'] && is_file($copy.'/storage/framework/down'), 'rollback preserves maintenance and never touches data schema');
$check(file_get_contents($copy.'/bootstrap/cache/config.php') === $beforeCache, 'rollback restores exact previous cache');
foreach ($manifest['host_overlays'] as $file => $values) {
$check(hash_file('sha256', $copy.'/'.$file) === $values['before_sha256'], 'restored original overlay '.$file);
}
$check(! is_dir($copy.'/packages/internal-requisitions') && is_dir($copy.'/storage/app/private/module-deployments/'.$id.'/withdrawn'), 'withdrawn bundle preserved for diagnosis');
$check(file_get_contents($copy.'/packages/purchasing/current.json') === $pointer, 'clone remains on original package pointer');
$failed = $make();
file_put_contents($failed.'/storage/framework/down', '{"status":503}');
$recover = new CodeDeployment($failed);
$plan2 = $recover->plan($bundle, $hash);
$deny(fn () => $recover->apply($bundle, $hash, $plan2['target_sha256'], 'Synthetic failure', true, function ($step) {
if (str_starts_with($step, 'file:')) {
throw new RuntimeException('Injected deployment interruption');
}
}), 'Injected deployment interruption');
$check(is_file($failed.'/storage/framework/down'), 'interruption retains maintenance');
$recover->rollback($id, 'Synthetic recovery', true);
foreach ($manifest['host_overlays'] as $file => $values) {
$check(hash_file('sha256', $failed.'/'.$file) === $values['before_sha256'], 'interrupted deployment recovers '.$file);
}
$check(file_get_contents($host.'/packages/purchasing/current.json') === $pointer, 'destination pointer untouched');
$switched = $make();
file_put_contents($switched.'/storage/framework/down', '{"status":503}');
$switchDeployment = new CodeDeployment($switched);
$switchPlan = $switchDeployment->plan($bundle, $hash);
$deny(fn () => $switchDeployment->apply($bundle, $hash, $switchPlan['target_sha256'], 'Synthetic commit failure', true, function ($step) {
if ($step === 'bundle-switched') {
throw new RuntimeException('Injected post-switch interruption');
}
}), 'Injected post-switch interruption');
$switchDeployment->rollback($id, 'Synthetic post-switch recovery', true);
$check(! is_dir($switched.'/packages/internal-requisitions') && is_file($switched.'/storage/framework/down'), 'post-switch interruption recovers without exposing partial code');
$lockPath = $switched.'/storage/app/private/module-deployments/deployment.lock';
$lock = fopen($lockPath, 'c+');
flock($lock, LOCK_EX);
$deny(fn () => $switchDeployment->rollback($id, 'Synthetic concurrent recovery', true), 'Outra implantação em curso');
flock($lock, LOCK_UN);
fclose($lock);
$cliCopy = $make();
file_put_contents($cliCopy.'/storage/framework/down', '{"status":503}');
$cli = function (array $args) use ($bundle) {
$command = implode(' ', array_map('escapeshellarg', [PHP_BINARY, $bundle.'/scripts/sgf-code-deploy.php', ...$args]));
$rows = [];
exec($command, $rows, $exit);
if ($exit !== 0) {
throw new RuntimeException('Code deployment CLI failed.');
}
return json_decode(implode("\n", $rows), true, 32, JSON_THROW_ON_ERROR);
};
$cliPlan = $cli(['plan', $cliCopy, $bundle, $hash]);
$check(! $cliPlan['writes'], 'CLI plan verifies bundle without writes');
$cliApplied = $cli(['apply', $cliCopy, $bundle, $hash, $cliPlan['target_sha256'], 'Synthetic CLI operator', '--offline']);
$check($cliApplied['state'] === 'applied' && $cliApplied['maintenance_preserved'], 'CLI apply leaves services and maintenance under operator control');
$cliRolled = $cli(['rollback', $cliCopy, $cliApplied['deployment_id'], 'Synthetic CLI recovery', '--offline']);
$check($cliRolled['state'] === 'rolled-back' && file_get_contents($cliCopy.'/packages/purchasing/current.json') === $pointer, 'CLI rollback preserves 0.1.1 pointer');
$proof = ['passed' => true, 'checks' => $checks, 'candidate' => $bundle, 'clones' => [$copy, $failed, $switched, $cliCopy], 'checked_at' => gmdate('c'), 'destination_version' => '0.1.1', 'destination_applied' => false, 'old_suites_repeated' => false, 'database_opened' => false, 'services_restarted' => false];
file_put_contents($root.'/docs/proofs/sgf-code-deploy-delivery.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)."\n";
+20
View File
@@ -0,0 +1,20 @@
<?php
use Tempo\Purchasing\Delivery\CodeDeployment;
require dirname(__DIR__).'/integrations/sgf/operational/CodeDeployment.php';
try {
$action = $argv[1] ?? '';
$deployment = new CodeDeployment($argv[2] ?? '');
$result = match ($action) {
'plan' => $deployment->plan($argv[3] ?? '', $argv[4] ?? ''),
'apply' => $deployment->apply($argv[3] ?? '', $argv[4] ?? '', $argv[5] ?? '', $argv[6] ?? '', ($argv[7] ?? '') === '--offline'),
'rollback' => $deployment->rollback($argv[3] ?? '', $argv[4] ?? '', ($argv[5] ?? '') === '--offline'),
'cache' => $deployment->rebuildCaches($argv[3] ?? '', $argv[4] ?? '', ($argv[5] ?? '') === '--offline'),
default => throw new RuntimeException('Usage: plan <host> <bundle> <manifest-sha> | apply <host> <bundle> <manifest-sha> <target-sha> <operator> --offline | rollback|cache <host> <deployment-id> <operator> --offline'),
};
echo json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)."\n";
} catch (Throwable $error) {
fwrite(STDERR, $error->getMessage()."\n");
exit(1);
}
+106
View File
@@ -0,0 +1,106 @@
<?php
use Dotenv\Dotenv;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
require dirname(__DIR__).'/vendor/autoload.php';
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$root = dirname(__DIR__);
$host = realpath($root.'/../sgf-tempo');
$config = DB::connection('pgsql')->getConfig();
if ($config['host'] !== '127.0.0.1') {
throw new RuntimeException('Local isolated cluster required.');
}
config(['database.connections.fixed_admin' => [...$config, 'url' => null, 'database' => 'postgres']]);
$admin = DB::connection('fixed_admin');
$suffix = bin2hex(random_bytes(6));
$owner = 'fix_owner_'.$suffix;
$runtime = 'fix_app_'.$suffix;
$database = 'sgf_reproduction_'.$suffix;
$ownerPassword = bin2hex(random_bytes(32));
$runtimePassword = bin2hex(random_bytes(32));
$roles = [];
$created = false;
$restoreCreated = false;
$restoreDatabase = 'sgf_reproduction_restore_'.$suffix;
$exit = 1;
$work = sys_get_temp_dir().'/sgf-corrected-'.$suffix;
mkdir($work, 0700);
$pointer = file_get_contents($host.'/packages/purchasing/current.json');
try {
$sourceSettings = Dotenv::parse(file_get_contents($host.'/.env'));
$sourceEnvironment = array_intersect_key($sourceSettings, array_flip(['DB_CONNECTION', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD', 'APP_ENV']));
$sourceEnvironment += ['APP_BASE_PATH' => $host, 'DB_URL' => $sourceSettings['DB_URL'] ?? '', 'APP_CONFIG_CACHE' => $host.'/bootstrap/cache/config.php'];
$export = new Process(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $host.'/scripts/sgf.ps1', 'php', $root.'/tests/Support/sgf-corrected-export.php'], $host, [...$sourceEnvironment, 'FIX_SOURCE_HOST' => $host, 'FIX_SCHEMA_FILE' => $work.'/source-schema.dump'], null, 90);
$export->mustRun();
$inventory = json_decode($export->getOutput(), true, 64, JSON_THROW_ON_ERROR);
if ($inventory['database'] !== $sourceSettings['DB_DATABASE']) {
throw new RuntimeException('Wrong reproduction source.');
}
file_put_contents($work.'/baseline.json', json_encode($inventory, JSON_THROW_ON_ERROR));
$prepare = new Process([PHP_BINARY, $root.'/scripts/prepare-sgf-operational.php', $host], $root);
$prepare->mustRun();
$artifact = json_decode($prepare->getOutput(), true, 16, JSON_THROW_ON_ERROR);
require $root.'/tests/Support/sgf-update-fixture.php';
$manifest = json_decode(file_get_contents($artifact['directory'].'/manifest.json'), true, 32, JSON_THROW_ON_ERROR);
$clone = updateFixture($host, $manifest, true);
foreach ([$owner => $ownerPassword, $runtime => $runtimePassword] as $role => $password) {
$admin->statement('CREATE ROLE "'.$role.'" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS PASSWORD '.$admin->getPdo()->quote($password));
$roles[] = $role;
}
$admin->statement('CREATE DATABASE "'.$database.'" OWNER "'.$owner.'"');
$created = true;
$env = ['APP_ENV' => 'production', 'APP_DEBUG' => 'false', 'APP_BASE_PATH' => $clone, 'APP_KEY' => 'base64:'.base64_encode(random_bytes(32)), 'DB_CONNECTION' => 'pgsql', 'DB_URL' => '', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => (string) $config['port'], 'DB_DATABASE' => $database, 'DB_USERNAME' => $owner, 'DB_PASSWORD' => $ownerPassword, 'SESSION_DRIVER' => 'file', 'SESSION_FILES' => $clone.'/storage/framework/sessions', 'SESSION_COOKIE' => 'corrected_'.$suffix, 'CACHE_STORE' => 'array', 'QUEUE_CONNECTION' => 'sync',
'FIX_SOURCE_HOST' => $host, 'FIX_CLONE' => $clone, 'FIX_BUNDLE' => $artifact['directory'], 'FIX_ROOT' => $root, 'FIX_WORK' => $work, 'FIX_RUNTIME' => $runtime, 'FIX_RUNTIME_PASSWORD' => $runtimePassword];
$restore = new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe', '--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$database, $work.'/source-schema.dump'], null, ['PGHOST' => '127.0.0.1', 'PGPORT' => (string) $config['port'], 'PGUSER' => $owner, 'PGPASSWORD' => $ownerPassword], null, 60);
$restore->mustRun();
$proofProcess = new Process(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $host.'/scripts/sgf.ps1', 'php', $root.'/tests/Support/sgf-corrected-proof.php'], $host, $env, null, 180);
$proofProcess->mustRun();
try {
$proof = json_decode($proofProcess->getOutput(), true, 64, JSON_THROW_ON_ERROR);
} catch (JsonException) {
file_put_contents($work.'/failure.txt', $proofProcess->getOutput().$proofProcess->getErrorOutput());
throw new RuntimeException('Private diagnostic '.$work.'/failure.txt');
}
file_put_contents($work.'/expected.json', json_encode($proof['data_fingerprints'], JSON_THROW_ON_ERROR));
$admin->statement('CREATE DATABASE "'.$restoreDatabase.'" OWNER "'.$owner.'"');
$restoreCreated = true;
$restoreData = new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe', '--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$restoreDatabase, $work.'/installed.dump'], null, ['PGHOST' => '127.0.0.1', 'PGPORT' => (string) $config['port'], 'PGUSER' => $owner, 'PGPASSWORD' => $ownerPassword], null, 90);
$restoreData->mustRun();
$readRestore = new Process(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $host.'/scripts/sgf.ps1', 'php', $root.'/tests/Support/sgf-corrected-restored.php'], $host, [...$env, 'DB_DATABASE' => $restoreDatabase], null, 60);
$readRestore->mustRun();
$proof['data_recovery'] = json_decode($readRestore->getOutput(), true, 32, JSON_THROW_ON_ERROR);
$after = new Process(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $host.'/scripts/sgf.ps1', 'php', $root.'/tests/Support/sgf-corrected-export.php'], $host, [...$sourceEnvironment, 'FIX_SOURCE_HOST' => $host, 'FIX_CHECK_ONLY' => '1'], null, 60);
$after->mustRun();
$afterState = json_decode($after->getOutput(), true, 64, JSON_THROW_ON_ERROR);
if ($afterState['source_security_fingerprints'] !== $inventory['source_security_fingerprints'] || $afterState['tables'] !== $inventory['tables']) {
throw new RuntimeException('Destination changed during isolated proof; reconcile.');
}
$proof['destination_security_and_schema_preserved'] = true;
if (! $proof['passed'] || file_get_contents($host.'/packages/purchasing/current.json') !== $pointer) {
throw new RuntimeException('Proof or destination preservation failed.');
}
$proof += ['artifact' => $artifact, 'source_schema_tables' => count($inventory['tables']), 'source_rows_exported' => 0, 'source_schema_sha256' => $inventory['schema_sha256'], 'runtime_and_migrator_separate' => true, 'destination_version' => '0.1.1', 'destination_changed' => false, 'prior_proofs_reused' => true, 'checked_at' => gmdate('c')];
$exit = 0;
} catch (Throwable $error) {
fwrite(STDERR, $error->getMessage()."\n");
} finally {
if ($restoreCreated) {
$admin->select('select pg_terminate_backend(pid) from pg_stat_activity where datname=? and pid<>pg_backend_pid()', [$restoreDatabase]);
$admin->statement('DROP DATABASE "'.$restoreDatabase.'"');
}
if ($created) {
$admin->select('select pg_terminate_backend(pid) from pg_stat_activity where datname=? and pid<>pg_backend_pid()', [$database]);
$admin->statement('DROP DATABASE "'.$database.'"');
}foreach (array_reverse($roles) as $role) {
$admin->statement('DROP ROLE "'.$role.'"');
}
}
if ($exit === 0) {
$proof['test_database_and_roles_removed'] = true;
file_put_contents($root.'/docs/proofs/sgf-corrected-delivery.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode($proof,JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)."\n";
}exit($exit);
+88
View File
@@ -0,0 +1,88 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
const root='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939';
const privateData=JSON.parse(fs.readFileSync(root+'/private/development.json','utf8'));
assert.deepEqual([privateData.tenant_id,privateData.company_id,privateData.unit_id],[1,1,1]);
const stateFile=root+'/private/requisitions-recovery/http-state.json';
const state=fs.existsSync(stateFile)?JSON.parse(fs.readFileSync(stateFile,'utf8')):{};
const persist=()=>fs.writeFileSync(stateFile,JSON.stringify(state,null,2));
const key=name=>{state.keys??={};state.keys[name]??=crypto.randomUUID();persist();return state.keys[name];};
const checks=[];
const clients={};
for(const p of privateData.profiles){
const jar=new Map();let csrf='',token='';
const api=async(path,method='GET',body,expected=200,commandKey)=>{
const r=await fetch('http://127.0.0.1:8039'+path,{method,headers:{Accept:'application/json','Content-Type':'application/json',Cookie:[...jar].map(([k,v])=>k+'='+v).join('; '),'X-CSRF-TOKEN':csrf,'X-SGF-Context':token,'X-Purchasing-Context':'procurement.internal','Idempotency-Key':commandKey??crypto.randomUUID()},...(body===undefined?{}:{body:JSON.stringify(body)})});
for(const cookie of r.headers.getSetCookie()){const pair=cookie.split(';')[0],at=pair.indexOf('=');jar.set(pair.slice(0,at),pair.slice(at+1));}
const text=await r.text();let d;try{d=JSON.parse(text);}catch{d={message:text.slice(0,120)};}
assert((Array.isArray(expected)?expected:[expected]).includes(r.status),`${p.role} ${path}: expected ${expected}, got ${r.status}: ${d.message??text.slice(0,180)}`);
if(d.csrf)csrf=d.csrf;if(d.context?.token)token=d.context.token;
return d;
};
await api('/api/session');await api('/api/login','POST',{email:p.email,password:privateData.password});
await api('/api/context','POST',{tenant_id:1,company_id:1,unit_id:1});clients[p.role]=api;
}
checks.push('Existing three synthetic identities authenticate without password changes');
const prep=clients.preparador,review=clients.revisor,read=clients.consulta;
const rights={1:['purchasing_requisitions.view','purchasing_requisitions.prepare','purchasing_requisitions.configure','purchasing_requisitions.approve.department_head','purchasing_budget.view','purchasing_budget.create','purchasing_budget.approve','purchasing_budget.record'],2:['purchasing_requisitions.view','purchasing_requisitions.approve.department_head','purchasing_budget.view','purchasing_budget.approve'],3:['purchasing_requisitions.view','purchasing_budget.view']};
await prep('/api/internal-requisitions/access-initialisation','POST',{operation_key:key('initial'),reason:'Owner-confirmed fourteen rights in synthetic demonstration 8039 only.',administrators:[{user_id:1,rights:rights[1]}]});
for(const [role,id] of [['revisor',2],['consulta',3]]){
const profile=privateData.profiles.find(p=>p.role===role);
for(const permission of rights[id])await prep('/api/accesses','POST',{email:profile.email,unit_id:1,permission,reason:'Owner-confirmed synthetic demonstration access 8039.'});
}
checks.push('Eight plus four plus two explicit authorised grants');
for(const api of Object.values(clients))await api('/api/context','POST',{tenant_id:1,company_id:1,unit_id:1,module_id:'purchasing'});
const centre=await prep('/api/internal-requisitions/centres','POST',{operation_key:key('centre'),code:'SINTETICO-REPRO',reason:'Owner-authorised synthetic centre for demonstration only.'});
state.centre_id=centre.centre_id;persist();
await read('/api/internal-requisitions/centres','POST',{operation_key:key('read-centre'),code:'REFUSED',reason:'Synthetic read-only refusal verification.'},403);
if(!state.budget_id){
await prep('/api/purchasing-budget/create','POST',{cost_center_id:state.centre_id,year:2026,kind:'operating',includes_tax:true,amount:'10000.00',reason:'Owner-authorised synthetic 10000 MZN allocation for 2026.'},200,key('budget'));
const list=await prep('/api/purchasing-budget');
fs.writeFileSync(root+'/private/requisitions-recovery/budget-response.json',JSON.stringify(list,null,2));
state.budget_pending=true;persist();
}
// Obtain exact budget identifier from the native response; no guessing IDs.
const workspace=await prep('/api/purchasing-budget');
const candidates=workspace.budgets??workspace.allocations??workspace.records;
if(!state.budget_id){
assert(Array.isArray(candidates),'Inspect native budget response shape before proceeding');
const found=candidates.filter(b=>Number(b.cost_center_id??b.centre_id)===Number(state.centre_id)&&Number(b.year)===2026);
assert.equal(found.length,1,'Exactly one synthetic allocation');state.budget_id=found[0].id;persist();
}
if(!state.budget_approved){
await prep('/api/purchasing-budget/approve','POST',{budget_id:state.budget_id},403,key('self-budget'));
await read('/api/purchasing-budget/approve','POST',{budget_id:state.budget_id},403,key('read-budget'));
await review('/api/purchasing-budget/approve','POST',{budget_id:state.budget_id},200,key('approve-budget'));
state.budget_approved=true;persist();
}
checks.push('Synthetic allocation 10000 MZN approved by second identity; self and read-only approvals refused');
const policy={version:0,reason:'Owner-confirmed synthetic OPER policy, demonstration 8039 only.',enabled:true,budget_required:true,first_limit_minor:1000000,second_limit_minor:2000000,approval_includes_tax:true,centres:[{id:state.centre_id,department_ref:'OPER'}],members:[{user_id:1,department_ref:'OPER'},{user_id:2,department_ref:'OPER'}]};
await read('/api/internal-requisitions/policy','POST',policy,403);
if(!state.policy_configured){await prep('/api/internal-requisitions/policy','POST',policy);state.policy_configured=true;persist();}
const draft={operation_key:key('draft'),cost_center_id:state.centre_id,priority:'normal',needed_by:'2026-11-30',justification:'Synthetic demonstration requisition, no Tempo business operation.',items:[{description:'Synthetic demonstration material',category:'Consumiveis',purchase_type:'consumable',quantity:'1.000',unit:'UN',unit_price:'100.00',tax_rate:'0.00'}]};
await read('/api/internal-requisitions/save','POST',{...draft,operation_key:key('read-save')},403);
if(!state.request_id){const saved=await prep('/api/internal-requisitions/save','POST',draft);state.request_id=saved.id;persist();}
const path='/api/internal-requisitions/'+state.request_id;
if(!state.submitted){
const submitted=await prep(path+'/submit','POST',{operation_key:key('submit'),version:1});
state.submitted=submitted;persist();
const replay=await prep(path+'/submit','POST',{operation_key:key('submit'),version:1});
assert.equal(replay.id,state.request_id);checks.push('Submission retry reuses same requisition');
}
if(!state.approved){
const decision={operation_key:key('decision'),version:state.submitted.version,decision:'approved'};
await prep(path+'/decide','POST',decision,403);
await read(path+'/decide','POST',decision,[403,404]);
await read(path+'/submit','POST',{operation_key:key('read-submit'),version:state.submitted.version},[403,404]);
const approved=await review(path+'/decide','POST',decision);assert.equal(approved.status,'approved');state.approved=true;persist();
}
for(const api of [prep,review]){const d=await api(path);assert.equal(d.record.status,'approved');assert.equal(d.history.length,3);}
await read(path,'GET',undefined,404);
await read('/api/internal-requisitions');
await read('/api/accesses','GET',undefined,403);
checks.push('Full request lifecycle, independent approval and three-event history consulted by preparer and reviewer');
checks.push('Existing record visibility rule returns 404 to read-only third identity; listing remains accessible; no extra rights granted');
checks.push('Read-only profile refused centre, budget approval, policy, preparation, submission, decision and access administration');
fs.writeFileSync('C:/laragon/www/compras/docs/proofs/sgf-demo-8039-http.json',JSON.stringify({passed:true,url:'http://127.0.0.1:8039',scope:'1:1:1',rights,centre_id:state.centre_id,budget_id:state.budget_id,request_id:state.request_id,checks,checked_at:new Date().toISOString(),visual_review:false,real_installation_changed:false},null,2));
console.log(JSON.stringify({passed:true,checks,request_id:state.request_id}));
+84
View File
@@ -0,0 +1,84 @@
<?php
// Explicitly authorised persistent synthetic demonstration only. No GRANT/REVOKE.
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
use Tempo\Purchasing\Delivery\CodeDeployment;
use Tempo\Purchasing\Delivery\Installation;
$root=dirname(__DIR__);
$host='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/development-app';
$action=$argv[1]??'';
$recovery='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/requisitions-recovery';
$proof=json_decode(file_get_contents($root.'/docs/proofs/sgf-corrected-delivery.json'),true,64,JSON_THROW_ON_ERROR);
$bundle=$proof['artifact']['directory'];
require $host.'/vendor/autoload.php';
$app=require $host.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$db=DB::connection();
$identity=$db->selectOne("select current_database() as db,current_user as role,current_setting('cluster_name') as cluster,inet_server_port() as port");
if ($app->basePath()!==str_replace('/',DIRECTORY_SEPARATOR,$host) && str_replace('\\','/',$app->basePath())!==$host) throw new RuntimeException('Wrong application folder');
if ($identity->db!=='sgf_modules_dev_2576427d1939'||$identity->role!=='sgf_modules_qa_2576427d1939'||$identity->cluster!=='sgf_modular_regression_2576427d1939'||(int)$identity->port!==55439||config('app.url')!=='http://127.0.0.1:8039') throw new RuntimeException('Wrong isolated identity');
if (!$db->table('units')->where(['id'=>1,'tenant_id'=>1,'company_id'=>1])->exists()) throw new RuntimeException('Wrong scope');
$private=json_decode(file_get_contents(dirname($host).'/private/development.json'),true,32,JSON_THROW_ON_ERROR);
if ([$private['tenant_id'],$private['company_id'],$private['unit_id']]!==[1,1,1]) throw new RuntimeException('Wrong synthetic provenance');
foreach([1=>'Preparador',2=>'Revisor',3=>'Consulta'] as $id=>$suffix) if(!str_ends_with($db->table('users')->where('id',$id)->value('name'),$suffix)) throw new RuntimeException('Wrong synthetic user');
if(!class_exists(Installation::class))require_once $bundle.'/integrations/sgf/operational/bootstrap.php';
require_once $bundle.'/integrations/sgf/operational/CodeDeployment.php';
$deploy=new CodeDeployment($host);
$mh=$proof['artifact']['archive']['manifest_sha256'];
if ($action==='prepare') {
if(is_dir($recovery)) throw new RuntimeException('Recovery already exists; inspect before retry');
$plan=$deploy->plan($bundle,$mh);
mkdir($recovery,0700,true);
$manifest=json_decode(file_get_contents($bundle.'/manifest.json'),true);
foreach([...array_keys($manifest['host_overlays']),'.env','config/modules.php','packages/purchasing/current.json'] as $file){
if(!is_dir(dirname($recovery.'/files/'.$file)))mkdir(dirname($recovery.'/files/'.$file),0700,true);
copy($host.'/'.$file,$recovery.'/files/'.$file);
}
$cfg=$db->getConfig();
$dump=new Process(['C:/Program Files/PostgreSQL/17/bin/pg_dump.exe','--format=custom','--file='.$recovery.'/before.dump','--dbname='.$identity->db],null,['PGHOST'=>'127.0.0.1','PGPORT'=>'55439','PGUSER'=>$identity->role,'PGPASSWORD'=>$cfg['password']],null,90);
$dump->mustRun();
(new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe','--list',$recovery.'/before.dump']))->mustRun();
$baseline=['identity'=>$identity,'host'=>$host,'code_plan'=>$plan,'backup_sha256'=>hash_file('sha256',$recovery.'/before.dump'),'grants'=>$db->table('permission_grants')->orderBy('id')->get()->all(),'users_sha256'=>hash('sha256',json_encode($db->table('users')->orderBy('id')->get())),'pointer_sha256'=>hash_file('sha256',$host.'/packages/purchasing/current.json')];
file_put_contents($recovery.'/baseline.json',json_encode($baseline,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo json_encode(['prepared'=>true,'identity'=>$identity,'host'=>$host,'recovery'=>$recovery,'backup_sha256'=>$baseline['backup_sha256'],'code_plan'=>$plan],JSON_PRETTY_PRINT);
} elseif($action==='install') {
$baseline=json_decode(file_get_contents($recovery.'/baseline.json'),true,64,JSON_THROW_ON_ERROR);
if(!hash_equals($baseline['backup_sha256'],hash_file('sha256',$recovery.'/before.dump')))throw new RuntimeException('Backup changed');
if(json_decode(json_encode($db->table('permission_grants')->orderBy('id')->get()),true)!==$baseline['grants'])throw new RuntimeException('Grants changed since backup');
if(is_file($host.'/storage/framework/down'))throw new RuntimeException('Unexpected prior maintenance');
file_put_contents($host.'/storage/framework/down',json_encode(['time'=>time(),'retry'=>60]));
$migration=Installation::migrate($db,Installation::target($db),$baseline['backup_sha256'],'Owner-authorised synthetic 8039 setup');
$applied=$deploy->apply($bundle,$mh,$baseline['code_plan']['target_sha256'],'Owner-authorised synthetic 8039 setup',true);
file_put_contents($recovery.'/applied.json',json_encode($applied,JSON_PRETTY_PRINT));
$config=['enabled'=>true,'target'=>Installation::target($db),'scopes'=>['1:1:1']];
file_put_contents($host.'/config/purchasing-operational.php','<?php return '.var_export($config,true).';');
file_put_contents($host.'/config/purchasing-internal.php','<?php return ["candidate_enabled"=>true];');
file_put_contents($host.'/config/purchasing-access.php','<?php return ["initialisation_scopes"=>["1:1:1"]];');
// The development server reloads PHP for every request; keep normal uncached config.
foreach(['config.php','routes-v7.php','events.php'] as $file)if(is_file($host.'/bootstrap/cache/'.$file))throw new RuntimeException('Unexpected native cache; retain maintenance and reconcile');
unlink($host.'/storage/framework/down');
echo json_encode(['installed_synthetic_only'=>true,'migration'=>$migration,'deployment'=>$applied,'recovery'=>$recovery],JSON_PRETTY_PRINT);
} elseif($action==='verify') {
$baseline=json_decode(file_get_contents($recovery.'/baseline.json'),true,64,JSON_THROW_ON_ERROR);
$http=json_decode(file_get_contents($root.'/docs/proofs/sgf-demo-8039-http.json'),true,64,JSON_THROW_ON_ERROR);
$current=$db->table('permission_grants')->orderBy('id')->get()->all();
$oldById=[];foreach($current as $grant)$oldById[$grant->id]=json_decode(json_encode($grant),true);
foreach($baseline['grants'] as $grant)if(($oldById[$grant['id']]??null)!==$grant)throw new RuntimeException('Existing grant changed');
$oldIds=array_column($baseline['grants'],'id');
$added=array_values(array_filter($current,fn($g)=>!in_array($g->id,$oldIds,true)));
if(count($added)!==14)throw new RuntimeException('Expected exactly fourteen new grants');
foreach($added as $grant)if([$grant->tenant_id,$grant->company_id,$grant->unit_id]!==[1,1,1]||!in_array($grant->permission,$http['rights'][$grant->user_id]??[],true))throw new RuntimeException('Unapproved new grant');
if(hash('sha256',json_encode($db->table('users')->orderBy('id')->get()))!==$baseline['users_sha256'])throw new RuntimeException('Existing accounts changed');
if(hash_file('sha256',$host.'/packages/purchasing/current.json')!==$baseline['pointer_sha256'])throw new RuntimeException('Pointer changed');
$requests=$db->table('ir_requests')->where(['tenant_id'=>1,'company_id'=>1,'unit_id'=>1])->get();
if(count($requests)!==1||$requests[0]->status!=='approved'||$requests[0]->requester_id!==1)throw new RuntimeException('Unexpected request state');
if($db->table('cb_budget_entries')->where('kind','reserve')->count()!==1)throw new RuntimeException('Expected exactly one reservation');
$budget=$db->table('cb_budgets')->first();
$decision=$db->table('ir_steps')->first();
$report=['passed'=>true,'identity'=>$identity,'scope'=>'1:1:1','added_grants'=>$added,'existing_grants_preserved'=>true,'existing_accounts_preserved'=>true,'single_approved_request'=>true,'single_reservation'=>true,'reservation'=>$db->table('cb_budget_entries')->where('kind','reserve')->first(),'budget'=>$budget,'decision'=>$decision,'pointer_preserved'=>true,'backup_sha256'=>$baseline['backup_sha256'],'recovery_directory'=>$recovery,'checked_at'=>gmdate('c'),'owner_runtime_limitation'=>true,'visual_review'=>false,'read_only_record_visibility_limitation'=>true];
file_put_contents($root.'/docs/proofs/sgf-demo-8039-delivery.json',json_encode($report,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
file_put_contents($host.'/config/purchasing-access.php','<?php return ["initialisation_scopes"=>[]];');
echo json_encode($report,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR);
} else throw new RuntimeException('Use prepare, install or verify');
+53
View File
@@ -0,0 +1,53 @@
<?php
// Read-only decision inventory. No migration, grants, backup or service control.
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
$host = realpath($argv[1] ?? '');
$candidate = realpath($argv[2] ?? '');
if (! $host || ! $candidate) {
fwrite(STDERR, "Explicit host and candidate required.\n");
exit(1);
}
require $host.'/vendor/autoload.php';
$app = require $host.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$db = null;
try {
$db = DB::connection();
if ($db->getDriverName() !== 'pgsql') {
throw new RuntimeException('PostgreSQL expected');
}
$db->beginTransaction();
$db->statement('SET TRANSACTION READ ONLY');
$identity = $db->selectOne('select current_database() as database, inet_server_addr()::text as address, inet_server_port() as port');
$tables = array_column($db->select("select tablename from pg_tables where schemaname='public' order by tablename"), 'tablename');
$required = ['users', 'tenants', 'companies', 'units', 'company_memberships', 'permission_grants', 'identity_audit'];
$budget = ['cb_centres', 'cb_origins', 'cb_budgets', 'cb_budget_entries', 'cb_budget_consumptions', 'cb_budget_reinforcements', 'cb_audit'];
$internal = ['ir_requests', 'ir_steps', 'ir_events', 'ir_commands', 'ir_policies', 'ir_policy_versions', 'sgc_installations'];
$column = $db->selectOne("select character_maximum_length from information_schema.columns where table_schema='public' and table_name='permission_grants' and column_name='permission'");
$constraint = $db->selectOne("select pg_get_constraintdef(oid) as definition from pg_constraint where conrelid='permission_grants'::regclass and conname='permission_grants_scope_check'");
$privileges = $db->selectOne("select has_schema_privilege(current_user,'public','CREATE') as can_create,pg_has_role(current_user,(select relowner from pg_class where oid='permission_grants'::regclass),'USAGE') as can_alter_permissions");
$preserve = [];
foreach (['permission_grants', 'company_memberships', 'identity_audit'] as $table) {
$rows = $db->select('select to_jsonb(t)::text as value from "'.$table.'" t order by to_jsonb(t)::text');
$preserve[$table] = ['rows' => count($rows), 'sha256' => hash('sha256', json_encode(array_column($rows, 'value'), JSON_THROW_ON_ERROR))];
}
$counts = ['companies' => $db->table('companies')->count(), 'units' => $db->table('units')->count(), 'company_admin_grants' => $db->table('permission_grants')->where('permission', 'access.manage')->whereNull('unit_id')->count()];
$roles = $db->selectOne("select current_user as runtime_role,(select pg_get_userbyid(relowner) from pg_class where oid='permission_grants'::regclass) as permissions_owner");
$ownerCapabilities = $db->selectOne("select rolcanlogin as can_login,has_schema_privilege(rolname,'public','CREATE') as can_create_public from pg_roles where rolname=?", [$roles->permissions_owner]);
$state = ['checked_at' => gmdate('c'), 'host' => $host, 'app_url' => config('app.url'), 'environment' => $app->environment(), 'php' => PHP_VERSION, 'database' => ['driver' => $db->getDriverName(), 'configured_host' => $db->getConfig('host'), 'configured_port' => $db->getConfig('port'), 'configured_database' => $db->getDatabaseName(), 'actual' => $identity],
'schema' => ['public_table_count' => count($tables), 'missing_foundation' => array_values(array_diff($required, $tables)), 'existing_budget_tables' => array_values(array_intersect($budget, $tables)), 'existing_integration_tables' => array_values(array_intersect($internal, $tables)), 'budget_ui_support_tables_present' => array_values(array_intersect(['cb_ui_commands', 'cb_consumption_sources'], $tables)), 'permission_column_length' => $column?->character_maximum_length, 'scope_constraint_present' => $constraint !== null, 'scope_constraint_sha256' => $constraint ? hash('sha256', $constraint->definition) : null, 'ddl_privileges' => $privileges],
'database_roles' => $roles, 'config_cached' => $app->configurationIsCached(),
'owner_capabilities' => $ownerCapabilities,
'preservation_baseline' => $preserve, 'organisation_counts' => $counts, 'purchasing_pointer' => json_decode(file_get_contents($host.'/packages/purchasing/current.json'), true, 16, JSON_THROW_ON_ERROR), 'pointer_sha256' => hash_file('sha256', $host.'/packages/purchasing/current.json'),
'bundle_exists' => is_dir($host.'/packages/internal-requisitions'), 'maintenance_marker_present' => is_file($host.'/storage/framework/down'), 'operational_config_file_present' => is_file($host.'/config/purchasing-operational.php'), 'read_only_transaction' => true, 'installed' => false];
$db->rollBack();
echo json_encode($state, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)."\n";
} catch (Throwable $error) {
if ($db && $db->transactionLevel() > 0) {
$db->rollBack();
}fwrite(STDERR, 'Read-only inventory failed: '.get_class($error)."\n");
exit(1);
}
+6 -3
View File
@@ -6,11 +6,12 @@ use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
use Tempo\Purchasing\Delivery\Installation;
use Tempo\Purchasing\Delivery\SqlPrivileges;
$root = dirname(__DIR__);
$action = $argv[1] ?? '';
$host = realpath($argv[2] ?? '');
if (! in_array($action, ['plan', 'migrate'], true) || ! $host) {
if (! in_array($action, ['plan', 'migrate', 'privileges-plan'], true) || ! $host) {
fwrite(STDERR, "Usage: plan <host> | migrate <host> <target-sha> <backup-file> <backup-sha> <operator> <manifest-sha>\n");
exit(1);
}
@@ -21,7 +22,7 @@ try {
throw new RuntimeException('Manifesto de preparação operacional obrigatório.');
}
foreach ($manifest['support_files'] as $file => $hash) {
if (($file !== 'bootstrap.php' && ! preg_match('~^(app|packages|integrations|scripts)/[A-Za-z0-9_./-]+$~D', $file)) || str_contains($file, '..') || is_link($root.'/'.$file) || ! hash_equals($hash, hash_file('sha256', $root.'/'.$file))) {
if (($file !== 'bootstrap.php' && ! preg_match('~^((app|packages|integrations|scripts)/[A-Za-z0-9_./-]+|resources/js/(app|PurchasingHome)\.jsx|public/build/manifest\.json)$~D', $file)) || str_contains($file, '..') || is_link($root.'/'.$file) || ! hash_equals($hash, hash_file('sha256', $root.'/'.$file))) {
throw new RuntimeException('Integridade da entrega divergente.');
}
}
@@ -36,7 +37,9 @@ try {
$app->make(Kernel::class)->bootstrap();
require $root.'/integrations/sgf/operational/bootstrap.php';
$db = DB::connection();
if ($action === 'plan') {
if ($action === 'privileges-plan') {
echo json_encode(SqlPrivileges::plan($argv[3] ?? ''), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)."\n";
} elseif ($action === 'plan') {
echo json_encode(['target_sha256' => Installation::target($db), 'manifest_sha256' => hash('sha256', $manifestText), 'schema_prepared' => $db->getSchemaBuilder()->hasTable('sgc_installations'), 'activated' => false], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)."\n";
} else {
$backup = $argv[4] ?? '';
+16
View File
@@ -0,0 +1,16 @@
param([ValidateSet('apply','rollback','reapply')][string]$Action)
$ErrorActionPreference='Stop'
$demoRoot='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/development-app'
$demoPhp='C:/Users/macbook/.config/investmoz/php-8.5.11/php.exe'
$demoProcesses=Get-CimInstance Win32_Process | Where-Object {$_.Name -eq 'php.exe' -and $_.CommandLine -match '\s-S\s+127\.0\.0\.1:8039\s'}
foreach($demoProcess in $demoProcesses){
if($demoProcess.ExecutablePath -ne $demoPhp.Replace('/','\') -or $demoProcess.CommandLine -notmatch '-t public\s+scripts/router\.php'){throw 'Unexpected 8039 process'}
Stop-Process -Id $demoProcess.ProcessId
}
$operationOutput=& $demoPhp C:/laragon/www/compras/scripts/sgf-read-8039.php $Action
if($LASTEXITCODE -ne 0){throw 'Update/recovery failed; keep 8039 stopped and inspect private journal'}
$operationResult=($operationOutput -join "`n") | ConvertFrom-Json
if(-not $operationResult.deployment_id){throw 'Operation did not return a deployment receipt; retain stopped state'}
$operationOutput
$logRoot='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/read-correction-recovery'
Start-Process -FilePath $demoPhp -ArgumentList @('-S','127.0.0.1:8039','-t','public','scripts/router.php') -WorkingDirectory $demoRoot -WindowStyle Hidden -RedirectStandardOutput "$logRoot/$Action-out.log" -RedirectStandardError "$logRoot/$Action-error.log"
+49
View File
@@ -0,0 +1,49 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
const legacy=process.argv.includes('--previous');
const privateData=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/development.json','utf8'));
assert.deepEqual([privateData.tenant_id,privateData.company_id,privateData.unit_id],[1,1,1]);
const checks=[];
for(const profile of privateData.profiles){
const jar=new Map();let csrf='',token='';
const api=async(path,method='GET',body,status=200,overrides={})=>{
const response=await fetch('http://127.0.0.1:8039'+path,{method,headers:{Accept:'application/json','Content-Type':'application/json',Cookie:[...jar].map(([k,v])=>k+'='+v).join('; '),'X-CSRF-TOKEN':csrf,'X-SGF-Context':token,'X-Purchasing-Context':'procurement.internal','Idempotency-Key':crypto.randomUUID(),...overrides},...(body===undefined?{}:{body:JSON.stringify(body)})});
for(const cookie of response.headers.getSetCookie()){const pair=cookie.split(';')[0],at=pair.indexOf('=');jar.set(pair.slice(0,at),pair.slice(at+1));}
const text=await response.text();let d;try{d=JSON.parse(text);}catch{d={};}
assert((Array.isArray(status)?status:[status]).includes(response.status),`${profile.role} ${path}: expected ${status}, got ${response.status}: ${d.message??''}`);
if(d.csrf)csrf=d.csrf;if(d.context?.token)token=d.context.token;
return d;
};
await api('/api/session');await api('/api/login','POST',{email:profile.email,password:privateData.password});
await api('/api/context','POST',{tenant_id:1,company_id:1,unit_id:1,module_id:'purchasing'});
const list=await api('/api/internal-requisitions');
if(legacy&&profile.role==='consulta'){
assert(!list.records.some(r=>r.id===1));await api('/api/internal-requisitions/1','GET',undefined,404);
checks.push('Previous version reproduces missing read-only listing/detail');continue;
}
assert(list.records.some(r=>r.id===1),'Existing request must be listed');
const detail=await api('/api/internal-requisitions/1');
assert.equal(detail.record.id,1);assert.equal(detail.record.version,3);assert.equal(detail.record.status,'approved');assert.equal(detail.record.total_minor,10000);
assert.equal(detail.history.length,3);
checks.push(`${profile.role}: existing request 1 listed, detail and three-event history readable`);
if(profile.role==='consulta'){
assert.equal(list.permissions.prepare,false);assert.equal(list.permissions.configure,false);assert.deepEqual(detail.record.allowed_actions,[]);
await api('/api/internal-requisitions/save','POST',{operation_key:crypto.randomUUID()},403);
for(const action of ['save','submit','decide'])await api('/api/internal-requisitions/1/'+action,'POST',{operation_key:crypto.randomUUID(),version:3,...(action==='decide'?{decision:'approved'}:{})},[403,404]);
await api('/api/internal-requisitions/policy','POST',{version:1,reason:'Denied read-only configuration verification.',enabled:true,budget_required:true,first_limit_minor:1000000,second_limit_minor:2000000,approval_includes_tax:true,centres:[{id:1,department_ref:'OPER'}],members:[{user_id:1,department_ref:'OPER'},{user_id:2,department_ref:'OPER'}]},403);
await api('/api/internal-requisitions/centres','POST',{operation_key:crypto.randomUUID(),code:'REFUSED',reason:'Read-only denial check.'},403);
await api('/api/purchasing-budget/approve','POST',{budget_id:1},403);
await api('/api/accesses','GET',undefined,403);
checks.push('Consulta: no allowed actions; creation, save, submission, decision, policy, centre, budget approval and administration refused');
await api('/api/internal-requisitions/1','GET',undefined,409,{'X-SGF-Context':'invalid'});
await api('/api/internal-requisitions/1','GET',undefined,403,{'X-Purchasing-Context':'procurement.third_party'});
await api('/api/internal-requisitions/999999999','GET',undefined,404);
await api('/api/context','POST',{tenant_id:1,company_id:1,unit_id:2,module_id:'purchasing'},403);
await api('/api/context','POST',{tenant_id:1,company_id:2,unit_id:3,module_id:'purchasing'},403);
checks.push('Consulta: forged context, wrong option, missing record and unauthorised company/unit refused');
}
}
const report={passed:true,mode:legacy?'previous-version-recovery':'corrected-version',url:'http://127.0.0.1:8039',scope:'1:1:1',request_id:1,request_recreated:false,reservation_commands_executed:false,rights_changed:false,checks,checked_at:new Date().toISOString(),visual_review:false};
fs.writeFileSync('C:/laragon/www/compras/docs/proofs/sgf-read-8039-'+(legacy?'rollback':'http')+'.json',JSON.stringify(report,null,2));
console.log(JSON.stringify(report));
+75
View File
@@ -0,0 +1,75 @@
<?php
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
use Tempo\Purchasing\Delivery\CodeDeployment;
$root=dirname(__DIR__);
$host='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/development-app';
$recovery=dirname($host).'/private/read-correction-recovery';
$candidate=json_decode(file_get_contents($root.'/docs/proofs/sgf-read-candidate.json'),true,32,JSON_THROW_ON_ERROR);
require $host.'/vendor/autoload.php';
$app=require $host.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$db=DB::connection();
$identity=$db->selectOne("select current_database() as db,current_user as role,inet_server_port() as port,current_setting('cluster_name') as cluster");
if(str_replace('\\','/',$app->basePath())!==$host||$identity->db!=='sgf_modules_dev_2576427d1939'||$identity->role!=='sgf_modules_qa_2576427d1939'||(int)$identity->port!==55439||$identity->cluster!=='sgf_modular_regression_2576427d1939'||config('app.url')!=='http://127.0.0.1:8039')throw new RuntimeException('Wrong demonstration');
$record=$db->table('ir_requests')->where(['id'=>1,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1])->first();
if(!$record||$record->status!=='approved'||$record->total_minor!==10000||$record->version!==3||$db->table('cb_budget_entries')->where('kind','reserve')->count()!==1)throw new RuntimeException('Wrong existing request');
require_once $root.'/integrations/sgf/operational/CodeDeployment.php';
$deploy=new CodeDeployment($host);
$fingerprints=function()use($db){
return $db->transaction(function()use($db){
$db->statement('SET TRANSACTION READ ONLY');$hashes=[];
foreach($db->select("select tablename from pg_tables where schemaname='public' order by tablename") as $t){
$rows=$db->select('select to_jsonb(t)::text as data from "'.$t->tablename.'" t order by to_jsonb(t)::text');
$hashes[$t->tablename]=hash('sha256',json_encode(array_column($rows,'data'),JSON_THROW_ON_ERROR));
}return $hashes;
});
};
$action=$argv[1]??'';
if($action==='prepare'){
if(is_dir($recovery))throw new RuntimeException('Recovery exists; no overwrite');
$plan=$deploy->plan($candidate['directory'],$candidate['archive']['manifest_sha256']);
mkdir($recovery,0700,true);
$cfg=$db->getConfig();
(new Process(['C:/Program Files/PostgreSQL/17/bin/pg_dump.exe','--format=custom','--file='.$recovery.'/before.dump','--dbname='.$identity->db],null,['PGHOST'=>'127.0.0.1','PGPORT'=>'55439','PGUSER'=>$identity->role,'PGPASSWORD'=>$cfg['password']],null,90))->mustRun();
(new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe','--list',$recovery.'/before.dump']))->mustRun();
$baseline=['identity'=>$identity,'plan'=>$plan,'data_fingerprints'=>$fingerprints(),'backup_sha256'=>hash_file('sha256',$recovery.'/before.dump'),'pointer_sha256'=>hash_file('sha256',$host.'/packages/purchasing/current.json'),'config_hashes'=>[]];
foreach(['.env','config/modules.php','config/purchasing-operational.php','config/purchasing-internal.php','config/purchasing-access.php'] as $file)$baseline['config_hashes'][$file]=hash_file('sha256',$host.'/'.$file);
file_put_contents($recovery.'/baseline.json',json_encode($baseline,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo json_encode(['prepared'=>true,'tables'=>count($baseline['data_fingerprints']),'backup_sha256'=>$baseline['backup_sha256'],'plan'=>$plan],JSON_PRETTY_PRINT);
}else{
$baseline=json_decode(file_get_contents($recovery.'/baseline.json'),true,64,JSON_THROW_ON_ERROR);
$currentFingerprints=$fingerprints();
$changedTables=array_keys(array_filter($currentFingerprints,fn($hash,$table)=>$hash!==($baseline['data_fingerprints'][$table]??null),ARRAY_FILTER_USE_BOTH));
$auditAppended=false;
if(in_array('identity_audit',$changedTables,true)){
$extract=new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe','--data-only','--table=identity_audit','--file=-',$recovery.'/before.dump']);$extract->mustRun();
if(!preg_match('~COPY public\.identity_audit \(id,[^)]*\) FROM stdin;\r?\n(.*?)\r?\n\\\\\.~s',$extract->getOutput(),$match))throw new RuntimeException('Audit baseline cannot be reconciled');
$ids=array_map(fn($line)=>(int)explode("\t",$line)[0],preg_split('/\r?\n/',$match[1]));
$rows=$db->select('select to_jsonb(t)::text as data from identity_audit t where id<=? order by to_jsonb(t)::text',[max($ids)]);
if(hash('sha256',json_encode(array_column($rows,'data'),JSON_THROW_ON_ERROR))!==$baseline['data_fingerprints']['identity_audit'])throw new RuntimeException('Existing audit modified');
$auditAppended=true;$changedTables=array_values(array_diff($changedTables,['identity_audit']));
}
if($changedTables!==[])throw new RuntimeException('Database rows changed: '.implode(', ',$changedTables));
if(hash_file('sha256',$host.'/packages/purchasing/current.json')!==$baseline['pointer_sha256'])throw new RuntimeException('Pointer changed');
foreach($baseline['config_hashes'] as $file=>$hash)if(hash_file('sha256',$host.'/'.$file)!==$hash)throw new RuntimeException('Configuration changed');
if($action==='verify'){
$report=['passed'=>true,'identity'=>$identity,'data_tables_reconciled'=>count($baseline['data_fingerprints']),'audit_appended_only'=>$auditAppended,'request_id'=>1,'request_version'=>3,'amount_minor'=>10000,'reservation_count'=>1,'rights_and_accounts_unchanged'=>true,'config_unchanged'=>true,'pointer_unchanged'=>true,'candidate_manifest_sha256'=>hash_file('sha256',$host.'/packages/internal-requisitions/manifest.json'),'backup_sha256'=>$baseline['backup_sha256'],'checked_at'=>gmdate('c')];
file_put_contents($root.'/docs/proofs/sgf-read-8039-data.json',json_encode($report,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode($report,JSON_PRETTY_PRINT);
}else{
if(is_file($host.'/storage/framework/down'))throw new RuntimeException('Unexpected maintenance');
file_put_contents($host.'/storage/framework/down',json_encode(['time'=>time(),'retry'=>60]));
if($action==='rollback'){
$applied=json_decode(file_get_contents($recovery.'/applied.json'),true);
$result=$deploy->rollback($applied['deployment_id'],'Authorised read fix recovery proof 8039',true);
file_put_contents($recovery.'/rollback.json',json_encode($result,JSON_PRETTY_PRINT));
}elseif(in_array($action,['apply','reapply'],true)){
$plan=$deploy->plan($candidate['directory'],$candidate['archive']['manifest_sha256']);
$result=$deploy->apply($candidate['directory'],$candidate['archive']['manifest_sha256'],$plan['target_sha256'],'Authorised read-only visibility correction 8039',true);
file_put_contents($recovery.'/'.($action==='apply'?'applied':'reapplied').'.json',json_encode($result,JSON_PRETTY_PRINT));
}else throw new RuntimeException('Use prepare/apply/rollback/reapply/verify');
unlink($host.'/storage/framework/down');echo json_encode($result,JSON_PRETTY_PRINT);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
// Freeze only the authorised adapter correction over the previously proved artifact.
$root=dirname(__DIR__);
$prior=json_decode(file_get_contents($root.'/docs/proofs/sgf-corrected-delivery.json'),true,64,JSON_THROW_ON_ERROR);
$old=$prior['artifact']['directory'];
$manifest=json_decode(file_get_contents($old.'/manifest.json'),true,64,JSON_THROW_ON_ERROR);
$file='integrations/sgf/internal-requisitions/Port.php';
$payload=[];
foreach($manifest['support_files'] as $path=>$hash){
$text=file_get_contents($old.'/'.$path);
if(!hash_equals($hash,hash('sha256',$text)))throw new RuntimeException('Prior artifact changed');
$payload[$path]=$path===$file?file_get_contents($root.'/'.$file):$text;
}
$manifest['support_files'][$file]=hash('sha256',$payload[$file]);
$manifestText=json_encode($manifest,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR);
$id='OPERATIONAL-CANDIDATE-'.substr(hash('sha256',$manifestText),0,16);
$dir=$root.'/storage/app/private/candidates/'.$id;
if(file_exists($dir))throw new RuntimeException('Candidate already exists; verify rather than overwrite');
mkdir($dir,0700,true);
foreach($payload+['manifest.json'=>$manifestText] as $path=>$text){
if(!is_dir(dirname($dir.'/'.$path)))mkdir(dirname($dir.'/'.$path),0700,true);
file_put_contents($dir.'/'.$path,$text);
}
$archive=$root.'/storage/app/private/candidate-archives/'.$id.'.zip';
$zip=new ZipArchive;
if($zip->open($archive,ZipArchive::CREATE|ZipArchive::EXCL)!==true)throw new RuntimeException('Archive collision');
foreach([...array_keys($payload),'manifest.json'] as $path)$zip->addFile($dir.'/'.$path,$path);
$zip->close();
$result=['directory'=>$dir,'archive'=>['file'=>$archive,'sha256'=>hash_file('sha256',$archive),'manifest_sha256'=>hash('sha256',$manifestText)],'prior_manifest_sha256'=>$prior['artifact']['archive']['manifest_sha256'],'changed_files'=>[$file],'schema_changed'=>false,'rights_changed'=>false,'installed_real'=>false];
file_put_contents($root.'/docs/proofs/sgf-read-candidate.json',json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR);
+24
View File
@@ -0,0 +1,24 @@
<?php
$root=dirname(__DIR__);
$old=json_decode(file_get_contents($root.'/docs/proofs/sgf-read-candidate.json'),true,32,JSON_THROW_ON_ERROR);
$manifest=json_decode(file_get_contents($old['directory'].'/manifest.json'),true,64,JSON_THROW_ON_ERROR);
$payload=[];
foreach($manifest['support_files'] as $file=>$hash){$text=file_get_contents($old['directory'].'/'.$file);if(hash('sha256',$text)!==$hash)throw new RuntimeException('Prior candidate changed');$payload[$file]=$text;}
foreach(['integrations/sgf/sourcing','packages/purchasing/src'] as $dir)foreach(new DirectoryIterator($root.'/'.$dir) as $file)if($file->isFile())$payload[$dir.'/'.$file->getFilename()]=file_get_contents($file->getPathname());
foreach(['integrations/sgf/operational/Installation.php','integrations/sgf/operational/CodeDeployment.php','integrations/sgf/access/AccessOverlay.php','integrations/sgf/operational/routes.php','integrations/sgf/internal-requisitions/Controller.php','integrations/sgf/internal-requisitions/screen.mjs'] as $file)$payload[$file]=file_get_contents($root.'/'.$file);
$payload['bootstrap.php']=file_get_contents($root.'/integrations/sgf/operational/entry.php');
$payload['scripts/sgf-sourcing-install.php']=file_get_contents($root.'/scripts/sgf-sourcing-install.php');
require_once $root.'/app/Services/ComparisonPolicy.php';
$payload['integrations/sgf/sourcing/comparison-defaults.json']=json_encode(App\Services\ComparisonPolicy::DEFAULTS,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR);
$file='app/Modules/Identity/Services/Permissions.php';
$payload[$file]=preg_replace('/public const UNIT = .*?;/s','public const UNIT = '.var_export(['unit.view','unit.edit',...requireRights($root)],true).';',$payload[$file],1);
$manifest['host_overlays'][$file]['after_sha256']=hash('sha256',$payload[$file]);
// The same native catalogue and delegation workflow remain in use.
foreach($payload as $file=>$text)$manifest['support_files'][$file]=hash('sha256',$text);
$manifestText=json_encode($manifest,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR);
$id='OPERATIONAL-CANDIDATE-'.substr(hash('sha256',$manifestText),0,16);$dir=$root.'/storage/app/private/candidates/'.$id;
if(file_exists($dir))throw new RuntimeException('Immutable candidate exists');mkdir($dir,0700,true);
foreach($payload+['manifest.json'=>$manifestText] as $file=>$text){if(!is_dir(dirname($dir.'/'.$file)))mkdir(dirname($dir.'/'.$file),0700,true);file_put_contents($dir.'/'.$file,$text);}
$archive=$root.'/storage/app/private/candidate-archives/'.$id.'.zip';$zip=new ZipArchive;if($zip->open($archive,ZipArchive::CREATE|ZipArchive::EXCL)!==true)throw new RuntimeException('Archive exists');foreach([...array_keys($payload),'manifest.json'] as $file)$zip->addFile($dir.'/'.$file,$file);$zip->close();
$out=['directory'=>$dir,'archive'=>['file'=>$archive,'sha256'=>hash_file('sha256',$archive),'manifest_sha256'=>hash('sha256',$manifestText)],'prior_manifest_sha256'=>$old['archive']['manifest_sha256'],'schema_extension'=>'sourcing revision 1; four ss_* tables and explicit catalogue extension','real_activated'=>false];file_put_contents($root.'/docs/proofs/sgf-sourcing-candidate.json',json_encode($out,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode($out,JSON_PRETTY_PRINT);
function requireRights(string $root):array{require_once $root.'/integrations/sgf/access/AccessOverlay.php';return Tempo\Purchasing\Delivery\AccessOverlay::RIGHTS;}
+35
View File
@@ -0,0 +1,35 @@
<?php
require dirname(__DIR__).'/vendor/autoload.php';
foreach(['QuotationPricing','ProposalScoring','RequisitionFailure','Sourcing'] as $c)require_once dirname(__DIR__).'/packages/purchasing/src/'.$c.'.php';
require_once dirname(__DIR__).'/app/Services/ComparisonPolicy.php';
$engine=new Tempo\Purchasing\Sourcing\Sourcing;
$policy=App\Services\ComparisonPolicy::DEFAULTS;
$request=['status'=>'approved','requester_id'=>1,'data'=>['items'=>[['description'=>'Synthetic material','quantity'=>'1.000','tax_rate'=>'0.00']]]];
$state=['version'=>0,'suppliers'=>[],'quotes'=>[],'proposals'=>[],'confirmed'=>null];$checks=[];
$check=function($ok,$label)use(&$checks){if(!$ok)throw new RuntimeException($label);$checks[]=$label;};
$deny=function($call,$status,$label)use($check){try{$call();throw new RuntimeException('Unexpected success: '.$label);}catch(Tempo\Purchasing\Internal\RequisitionFailure $e){$check($e->status===$status,$label);}};
$step=function($s,$actor,$action,$input)use($engine,$request,$policy){$next=$engine->transition($s,$request,$actor,$action,$input,$policy,'2026-11-02');$next['version']=$s['version']+1;return $next;};
foreach([1,2,3] as $id){$state=$step($state,1,'select',['supplier'=>['id'=>$id,'eligible'=>true,'snapshot'=>['name'=>'Synthetic '.$id]]]);$state=$step($state,1,'quote',['supplier_id'=>$id,'reference'=>'Q-'.$id,'currency'=>'MZN','valid_until'=>'2026-12-31','delivery_days'=>5,'payment_terms'=>'30 days','items'=>[['line'=>0,'unit_price'=>(string)(90+$id*10)]]]);}
$check(!$engine->comparison($state,$policy,'2026-11-02')['ready'],'Existing minimum independent evaluations retained');
$evaluation=fn($id)=>['supplier_id'=>$id,'quote_revision'=>1,'quality'=>8,'history'=>8,'payment'=>8,'reason'=>'Synthetic evaluation reason'];
$deny(fn()=>$step($state,1,'evaluate',$evaluation(1)),403,'Requester cannot evaluate');
foreach([1,2,3] as $id)foreach([2,4] as $actor)$state=$step($state,$actor,'evaluate',$evaluation($id));
$comparison=$engine->comparison($state,$policy,'2026-11-02');$check($comparison['ready']&&$comparison['top_ids']===[1],'Existing weighted scoring identifies best eligible quote');
$proposal=['supplier_id'=>1,'comparison_hash'=>$comparison['fingerprint'],'reason'=>'Synthetic award proposal justification','delivery_date'=>'2027-01-15','delivery_location'=>'Local de entrega exclusivamente sintético OPER'];
$deny(fn()=>$step($state,1,'propose',$proposal),403,'Requester cannot propose award');
$state=$step($state,2,'propose',$proposal);
$confirmation=['proposal_index'=>0,'comparison_hash'=>$comparison['fingerprint'],'reason'=>'Independent synthetic confirmation'];
$deny(fn()=>$step($state,2,'confirm',$confirmation),403,'Proposer cannot confirm own award');
$deny(fn()=>$step($state,1,'confirm',$confirmation),403,'Requester cannot confirm award');
$changed=$step($state,1,'quote',['supplier_id'=>1,'reference'=>'Q-1-r2','currency'=>'MZN','valid_until'=>'2026-12-31','delivery_days'=>6,'payment_terms'=>'30 days','items'=>[['line'=>0,'unit_price'=>'100.00']]]);
$deny(fn()=>$step($changed,4,'confirm',$confirmation),409,'Quote revision invalidates existing award basis and evaluations');
$revoked=$state;$revoked['quotes'][1]['evaluations'][4]['eligible']=false;
$deny(fn()=>$step($revoked,4,'confirm',$confirmation),409,'Evaluator revocation invalidates confirmation basis');
$expired=$state;$expired['quotes'][1]['valid_until']='2026-11-01';
$deny(fn()=>$step($expired,4,'confirm',$confirmation),409,'Expired quote invalidates confirmation');
$ok=$step($state,4,'confirm',$confirmation);
$check($ok['confirmed']['confirmed_by']===4&&$ok['confirmed']['proposal']['proposed_by']===2,'Independent confirmation preserves exact proposed comparison');
$deny(fn()=>$step($ok,1,'quote',[]),409,'Confirmed process cannot be rewritten');
$check($state['proposals'][0]['comparison']['fingerprint']===$comparison['fingerprint'],'Comparison snapshot and justification preserved');
$report=['passed'=>true,'checks'=>$checks,'synthetic_only'=>true,'persistent_demo_accounts_created'=>false,'checked_at'=>gmdate('c')];
file_put_contents(dirname(__DIR__).'/docs/proofs/sgf-sourcing-core.json',json_encode($report,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode($report,JSON_PRETTY_PRINT);
+15
View File
@@ -0,0 +1,15 @@
param([ValidateSet('apply','apply-code')][string]$Action='apply')
$ErrorActionPreference='Stop'
$demoRoot='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/development-app'
$demoPhp='C:/Users/macbook/.config/investmoz/php-8.5.11/php.exe'
foreach($process in (Get-CimInstance Win32_Process | Where-Object {$_.Name -eq 'php.exe' -and $_.CommandLine -match '\s-S\s+127\.0\.0\.1:8039\s'})){
if($process.ExecutablePath -ne $demoPhp.Replace('/','\') -or $process.CommandLine -notmatch '-t public\s+scripts/router\.php'){throw 'Unexpected 8039 web process'}
Stop-Process -Id $process.ProcessId
}
$output=& $demoPhp C:/laragon/www/compras/scripts/sgf-sourcing-demo.php $Action
if($LASTEXITCODE -ne 0){throw 'Sourcing preparation failed; keep demonstration closed and inspect recovery'}
$result=($output -join "`n")|ConvertFrom-Json
if(-not $result.applied){throw 'No valid application receipt'}
$output
$logs='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/sourcing-recovery'
Start-Process -FilePath $demoPhp -ArgumentList @('-S','127.0.0.1:8039','-t','public','scripts/router.php') -WorkingDirectory $demoRoot -WindowStyle Hidden -RedirectStandardOutput "$logs/$Action-server-out.log" -RedirectStandardError "$logs/$Action-server-error.log"
+26
View File
@@ -0,0 +1,26 @@
import fs from 'node:fs';import assert from 'node:assert/strict';import crypto from 'node:crypto';
const privateData=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/development.json','utf8'));
assert.deepEqual([privateData.tenant_id,privateData.company_id,privateData.unit_id],[1,1,1]);
const clients={},checks=[];
for(const p of privateData.profiles){let csrf='',token='';const jar=new Map();
const api=async(path,method='GET',body,status=200)=>{const r=await fetch('http://127.0.0.1:8039'+path,{method,headers:{Accept:'application/json','Content-Type':'application/json',Cookie:[...jar].map(([k,v])=>k+'='+v).join('; '),'X-CSRF-TOKEN':csrf,'X-SGF-Context':token,'X-Purchasing-Context':'procurement.internal'},...(body===undefined?{}:{body:JSON.stringify(body)})});for(const c of r.headers.getSetCookie()){const pair=c.split(';')[0],at=pair.indexOf('=');jar.set(pair.slice(0,at),pair.slice(at+1));}const text=await r.text();let d;try{d=JSON.parse(text);}catch{d={message:text.slice(0,160)};}assert.equal(r.status,status,`${p.role} ${path}: ${d.message??JSON.stringify(d.errors)}`);if(d.csrf)csrf=d.csrf;if(d.context?.token)token=d.context.token;return d;};
await api('/api/session');await api('/api/login','POST',{email:p.email,password:privateData.password});await api('/api/context','POST',{tenant_id:1,company_id:1,unit_id:1,module_id:'purchasing'});clients[p.role]=api;
}
const read=who=>clients[who]('/api/sourcing/1');
async function command(who,action,body,status=200){const current=await read(who);return clients[who]('/api/sourcing/1/'+action,'POST',{operation_key:crypto.randomUUID(),version:current.state.version,...(action==='propose'?{delivery_date:'2027-01-15',delivery_location:'Local de entrega apenas sintético para teste de recusa'}:{}),...body},status);}
let initial=await read('preparador');assert.equal(initial.request.total_minor,10000);assert.equal(initial.request.version,3);assert.equal(initial.suppliers.length,3);
for(const [index,supplier] of initial.suppliers.entries()){
let current=await read('preparador');if(!current.state.suppliers[supplier.id])await command('preparador','select',{supplier_id:supplier.id});
current=await read('preparador');
if(!current.state.quotes[supplier.id])await command('preparador','quote',{supplier_id:supplier.id,reference:'DEMO-COTACAO-'+supplier.reference,currency:'MZN',valid_until:'2027-12-31',delivery_days:5+index,payment_terms:'Condições sintéticas: pagamento a 30 dias',items:current.request.data.items.map((_,line)=>({line,unit_price:(100+index*10).toFixed(2)}))});
current=await read('revisor');const q=current.state.quotes[supplier.id];
if(!q.evaluations[2])await command('revisor','evaluate',{supplier_id:supplier.id,quote_revision:q.revision,quality:8,history:8,payment:8,no_conflict:true,reason:'Avaliação exclusivamente sintética de demonstração; sem contacto externo.'});
}
const pending=await read('consulta');assert.equal(pending.comparison.ready,false);assert(pending.comparison.rows.every(r=>r.evaluation_count===1));assert.equal(pending.state.confirmed,null);assert.equal(pending.state.proposals.length,0);
await command('revisor','propose',{supplier_id:pending.comparison.rows[0].supplier_id,comparison_hash:pending.comparison.fingerprint,reason:'Verificar bloqueio por falta de segundo avaliador independente.'},422);
await command('revisor','propose',{supplier_id:pending.comparison.rows[1].supplier_id,comparison_hash:pending.comparison.fingerprint,reason:'Verificar sinalização de adaptação orçamental sem movimento financeiro.'},409);
for(const action of ['select','quote','evaluate','propose','confirm'])await clients.consulta('/api/sourcing/1/'+action,'POST',{},403);
await clients.revisor('/api/sourcing/1/confirm','POST',{},403);
for(const who of ['preparador','revisor','consulta']){const origin=await clients[who]('/api/internal-requisitions/1');assert.equal(origin.record.version,3);assert.equal(origin.record.total_minor,10000);assert.equal(origin.history.length,3);assert(origin.canSourcing);await clients[who]('/purchasing/sourcing/1');}
checks.push('Existing request reused with version 3 and unchanged three-event history');checks.push('Three selected synthetic suppliers, three quotes and one eligible evaluation per quote');checks.push('Comparison and award proposal remain blocked without second eligible evaluator');checks.push('Quoted amount differing from reserved amount is signalled without a budget movement');checks.push('Consulta retains full sourcing read and no preparation/evaluation/proposal/confirmation');checks.push('No persistent identity has award-confirmation right; no award created');checks.push('Protected sourcing screen is accessible to all three existing profiles');
const proof={passed:true,url:'http://127.0.0.1:8039',scope:'1:1:1',request_id:1,checks,confirmation_completed:false,owner_decision_no_fourth_account:true,visual_review:false,checked_at:new Date().toISOString()};fs.writeFileSync('C:/laragon/www/compras/docs/proofs/sgf-sourcing-demo-http.json',JSON.stringify(proof,null,2));console.log(JSON.stringify(proof));
+65
View File
@@ -0,0 +1,65 @@
<?php
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
use App\Modules\Identity\Services\Permissions;
use App\Modules\Identity\Services\Audit;
use Tempo\Purchasing\Delivery\CodeDeployment;
use Tempo\Purchasing\SgfSourcing\Schema;
$root=dirname(__DIR__);$host='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/development-app';$recovery=dirname($host).'/private/sourcing-recovery';
require $host.'/vendor/autoload.php';$app=require $host.'/bootstrap/app.php';$app->make(Kernel::class)->bootstrap();
try{
$db=DB::connection();$identity=$db->selectOne("select current_database() as db,current_user as role,inet_server_port() as port,current_setting('cluster_name') as cluster");
if(str_replace('\\','/',$app->basePath())!==$host||$identity->db!=='sgf_modules_dev_2576427d1939'||$identity->role!=='sgf_modules_qa_2576427d1939'||(int)$identity->port!==55439||$identity->cluster!=='sgf_modular_regression_2576427d1939'||config('app.url')!=='http://127.0.0.1:8039')throw new RuntimeException('Wrong persistent synthetic demonstration');
app(Permissions::class)->require(1,1,1,'access.manage');
$candidate=json_decode(file_get_contents($root.'/docs/proofs/sgf-sourcing-candidate.json'),true,32,JSON_THROW_ON_ERROR);
if(!class_exists(Schema::class))require_once $candidate['directory'].'/integrations/sgf/sourcing/Schema.php';require_once $root.'/integrations/sgf/operational/CodeDeployment.php';
$hash=function(string $table)use($db){return hash('sha256',json_encode($db->table($table)->orderBy('id')->get(),JSON_THROW_ON_ERROR));};
$action=$argv[1]??'';
if($action==='prepare'){
if(is_dir($recovery))throw new RuntimeException('Recovery exists: do not overwrite');
$proof=json_decode(file_get_contents($root.'/docs/proofs/sgf-sourcing-isolated.json'),true);if(!$proof['passed']||!$proof['persistent_demo_unchanged']||!($proof['recovery']['data_schema_restore_verified']??false)||$proof['setup']['deployment']['deployment_id']!==substr($candidate['archive']['manifest_sha256'],0,24))throw new RuntimeException('Current candidate needs accepted isolation/recovery proof');
if($db->table('users')->count()!==3)throw new RuntimeException('Unexpected accounts: reconcile before proceeding');
mkdir($recovery,0700,true);$cfg=$db->getConfig();(new Process(['C:/Program Files/PostgreSQL/17/bin/pg_dump.exe','--format=custom','--file='.$recovery.'/before.dump','--dbname='.$identity->db],null,['PGHOST'=>'127.0.0.1','PGPORT'=>'55439','PGUSER'=>$identity->role,'PGPASSWORD'=>$cfg['password']],null,90))->mustRun();
(new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe','--list',$recovery.'/before.dump']))->mustRun();
$baseline=['candidate'=>$candidate,'identity'=>$identity,'backup_sha256'=>hash_file('sha256',$recovery.'/before.dump'),'users'=>$hash('users'),'requests'=>$hash('ir_requests'),'budget'=>$hash('cb_budget_entries'),'consumption'=>$hash('cb_budget_consumptions'),'grants'=>$db->table('permission_grants')->orderBy('id')->get()->all(),'pointer_sha256'=>hash_file('sha256',$host.'/packages/purchasing/current.json')];
file_put_contents($recovery.'/baseline.json',json_encode($baseline,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode(['prepared'=>true,'identity'=>$identity,'backup_sha256'=>$baseline['backup_sha256']]);
}elseif($action==='apply'){
$baseline=json_decode(file_get_contents($recovery.'/baseline.json'),true,64,JSON_THROW_ON_ERROR);
foreach(['users'=>'users','requests'=>'ir_requests','budget'=>'cb_budget_entries','consumption'=>'cb_budget_consumptions'] as $key=>$table)if($baseline[$key]!==$hash($table))throw new RuntimeException('Baseline changed: '.$table);
if($baseline['candidate']['archive']['manifest_sha256']!==$candidate['archive']['manifest_sha256']||!hash_equals($baseline['backup_sha256'],hash_file('sha256',$recovery.'/before.dump')))throw new RuntimeException('Candidate/backup changed');
if(is_file($host.'/storage/framework/down'))throw new RuntimeException('Unexpected prior maintenance');file_put_contents($host.'/storage/framework/down','{}');
$old=json_decode(file_get_contents($host.'/packages/internal-requisitions/manifest.json'),true);$current=json_decode(file_get_contents($host.'/storage/app/private/module-deployments/current.json'),true);$next=json_decode(file_get_contents($candidate['directory'].'/manifest.json'),true);
$migration=Schema::migrate($db,$baseline['backup_sha256'],'Owner-confirmed synthetic sourcing 8039');
$receipt=['host'=>$host,'from_manifest_sha256'=>$current['manifest_sha256'],'to_manifest_sha256'=>$candidate['archive']['manifest_sha256'],'schema_verified'=>Schema::compatible($db),'changes'=>[]];
foreach($next['support_files'] as $file=>$sha)if(($old['support_files'][$file]??null)!==$sha)$receipt['changes'][$file]=['before'=>$old['support_files'][$file]??null,'after'=>$sha];
file_put_contents($host.'/storage/app/private/module-deployments/schema-transition-'.$candidate['archive']['manifest_sha256'].'.json',json_encode($receipt));
$deploy=new CodeDeployment($host);$plan=$deploy->plan($candidate['directory'],$candidate['archive']['manifest_sha256']);$applied=$deploy->apply($candidate['directory'],$candidate['archive']['manifest_sha256'],$plan['target_sha256'],'Owner-confirmed synthetic sourcing 8039',true);file_put_contents($recovery.'/applied.json',json_encode($applied,JSON_PRETTY_PRINT));
$db->transaction(function()use($db){
foreach([1=>['purchasing_sourcing.prepare'],2=>['purchasing_sourcing.evaluate','purchasing_sourcing.propose']] as $id=>$rights)foreach($rights as $right){
$scope=['user_id'=>$id,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'permission'=>$right];$inserted=$db->table('permission_grants')->insertOrIgnore($scope+['created_at'=>now(),'updated_at'=>now()]);
if($inserted)Audit::record('access.granted',1,1,1,1,$id,details:['permission'=>$right,'reason'=>'Owner explicitly authorised exact sourcing map; no fourth persistent account.']);
}
foreach(['A','B','C'] as $letter)$db->table('ss_suppliers')->insertOrIgnore(['tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'reference'=>'SOURCING-DEMO-'.$letter,'payload'=>json_encode(['name'=>'Fornecedor sintético '.$letter,'active'=>true,'blocked'=>false,'review_until'=>'2027-12-31','version'=>1,'synthetic_only'=>true]),'created_at'=>now()]);
});
file_put_contents($host.'/config/purchasing-sourcing.php','<?php return ["enabled"=>true];');unlink($host.'/storage/framework/down');echo json_encode(['applied'=>true,'deployment'=>$applied,'schema'=>$migration,'persistent_accounts_created'=>false,'grants_added'=>3,'confirmation_blocked_without_independent_identity'=>true]);
}elseif($action==='apply-code'){
if(!Schema::compatible($db))throw new RuntimeException('Code-only update requires the existing compatible sourcing schema');
$proof=json_decode(file_get_contents($root.'/docs/proofs/sgf-sourcing-isolated.json'),true);if(!$proof['passed']||$proof['setup']['deployment']['deployment_id']!==substr($candidate['archive']['manifest_sha256'],0,24))throw new RuntimeException('Candidate proof mismatch');
$before=[];foreach([...Schema::TABLES,'ir_requests','cb_budget_entries','cb_budget_consumptions','permission_grants','users'] as $table)$before[$table]=$hash($table);
if(is_file($host.'/storage/framework/down'))throw new RuntimeException('Unexpected prior maintenance');
$deploy=new CodeDeployment($host);$plan=$deploy->plan($candidate['directory'],$candidate['archive']['manifest_sha256']);
file_put_contents($recovery.'/before-final-code.json',json_encode(['candidate'=>$candidate,'data_hashes'=>$before,'plan'=>$plan],JSON_PRETTY_PRINT));file_put_contents($host.'/storage/framework/down','{}');
$applied=$deploy->apply($candidate['directory'],$candidate['archive']['manifest_sha256'],$plan['target_sha256'],'Authorised sourcing final code refinement',true);
foreach($before as $table=>$expected)if($hash($table)!==$expected)throw new RuntimeException('Code update changed data: '.$table);
file_put_contents($recovery.'/final-code.json',json_encode($applied,JSON_PRETTY_PRINT));unlink($host.'/storage/framework/down');echo json_encode(['applied'=>true,'code_only'=>true,'deployment'=>$applied,'data_preserved'=>true,'schema_changed'=>false,'rights_changed'=>false]);
}elseif($action==='verify'){
if(hash_file('sha256',$host.'/packages/internal-requisitions/manifest.json')!==$candidate['archive']['manifest_sha256'])throw new RuntimeException('Installed candidate does not match the delivery report');
$baseline=json_decode(file_get_contents($recovery.'/baseline.json'),true,64,JSON_THROW_ON_ERROR);foreach(['users'=>'users','requests'=>'ir_requests','budget'=>'cb_budget_entries','consumption'=>'cb_budget_consumptions'] as $key=>$table)if($baseline[$key]!==$hash($table))throw new RuntimeException('Protected baseline changed: '.$table);
$current=$db->table('permission_grants')->orderBy('id')->get()->keyBy('id');foreach($baseline['grants'] as $old)if((array)$current[$old['id']]!=$old)throw new RuntimeException('Previous grant changed');
$new=$current->except(array_column($baseline['grants'],'id'))->values()->all();if(count($new)!==3)throw new RuntimeException('Expected exactly three authorised new rights');foreach($new as $g)if([$g->tenant_id,$g->company_id,$g->unit_id]!==[1,1,1]||!in_array([$g->user_id,$g->permission],[[1,'purchasing_sourcing.prepare'],[2,'purchasing_sourcing.evaluate'],[2,'purchasing_sourcing.propose']],true))throw new RuntimeException('Unapproved grant');
if(hash_file('sha256',$host.'/packages/purchasing/current.json')!==$baseline['pointer_sha256'])throw new RuntimeException('Pointer changed');
$process=$db->table('ss_processes')->where(['tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'request_id'=>1])->first();$state=json_decode($process->payload,true);if($state['confirmed']!==null||$state['proposals']!==[])throw new RuntimeException('Incomplete comparison must not be awarded');
$report=['passed'=>true,'identity'=>$identity,'scope'=>'1:1:1','candidate'=>$candidate,'protected_users_requisition_reservation_consumption_unchanged'=>true,'previous_grants_preserved'=>true,'new_grants'=>$new,'persistent_accounts_created'=>false,'proposals'=>0,'confirmed'=>false,'suppliers_selected'=>count($state['suppliers']),'quotations'=>count($state['quotes']),'pending'=>'Second eligible evaluator and independent confirmer absent; no account created by owner decision.','backup_sha256'=>$baseline['backup_sha256'],'recovery_directory'=>$recovery,'checked_at'=>gmdate('c')];file_put_contents($root.'/docs/proofs/sgf-sourcing-demo.json',json_encode($report,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode($report,JSON_PRETTY_PRINT);
}else throw new RuntimeException('Use prepare/apply/verify');
}catch(Throwable $e){fwrite(STDERR,$e->getMessage()."\n");exit(1);}
+31
View File
@@ -0,0 +1,31 @@
<?php
// Offline additive migration; no rights, credentials, contacts or activation are provisioned.
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
use Tempo\Purchasing\Delivery\Installation;
use Tempo\Purchasing\Delivery\CodeDeployment;
use Tempo\Purchasing\SgfSourcing\Schema;
$root=dirname(__DIR__);$action=$argv[1]??'';$host=realpath($argv[2]??'');
try{
if(!$host||!in_array($action,['plan','migrate'],true))throw new RuntimeException('Usage: plan <host> | migrate <host> <target-sha> <backup-file> <backup-sha> <operator> <manifest-sha>');
$host=str_replace('\\','/',$host);$manifestText=file_get_contents($root.'/manifest.json');$manifest=json_decode($manifestText,true,64,JSON_THROW_ON_ERROR);$manifestHash=hash('sha256',$manifestText);
foreach($manifest['support_files'] as $file=>$hash)if(str_contains($file,'..')||!preg_match('~^[A-Za-z0-9_./-]+$~D',$file)||is_link($root.'/'.$file)||!hash_equals($hash,hash_file('sha256',$root.'/'.$file)))throw new RuntimeException('Bundle integrity failed');
require $host.'/vendor/autoload.php';$app=require $host.'/bootstrap/app.php';$app->make(Kernel::class)->bootstrap();
if(!class_exists(Installation::class))require_once $root.'/integrations/sgf/operational/bootstrap.php';
if(!class_exists(Schema::class))require_once $root.'/integrations/sgf/sourcing/Schema.php';
require_once $root.'/integrations/sgf/operational/CodeDeployment.php';
$db=DB::connection();$current=json_decode(file_get_contents($host.'/storage/app/private/module-deployments/current.json'),true,32,JSON_THROW_ON_ERROR);
(new CodeDeployment($host))->plan($host.'/packages/internal-requisitions',$current['manifest_sha256']);
$plan=['target_sha256'=>Installation::target($db),'manifest_sha256'=>$manifestHash,'schema_changes'=>Schema::TABLES,'new_rights'=>Schema::RIGHTS,'grant_performed'=>false,'activated'=>false];
if($action==='plan'){echo json_encode($plan,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR);exit;}
$backup=$argv[4]??'';$backupHash=$argv[5]??'';$operator=$argv[6]??'';
if(!hash_equals($plan['target_sha256'],$argv[3]??'')||!hash_equals($manifestHash,$argv[7]??'')||!is_file($backup)||!hash_equals($backupHash,hash_file('sha256',$backup))||trim($operator)===''||!is_file($host.'/storage/framework/down'))throw new RuntimeException('Target, manifest, backup, operator and maintenance are required');
(new Process([getenv('PG_RESTORE_BINARY')?:'pg_restore','--list',$backup],null,null,null,60))->mustRun();
$result=Schema::migrate($db,$backupHash,$operator);
$old=json_decode(file_get_contents($host.'/packages/internal-requisitions/manifest.json'),true,64,JSON_THROW_ON_ERROR);
$receipt=['host'=>$host,'from_manifest_sha256'=>$current['manifest_sha256'],'to_manifest_sha256'=>$manifestHash,'schema_verified'=>Schema::compatible($db),'changes'=>[]];
foreach($manifest['support_files'] as $file=>$hash)if(($old['support_files'][$file]??null)!==$hash)$receipt['changes'][$file]=['before'=>$old['support_files'][$file]??null,'after'=>$hash];
file_put_contents($host.'/storage/app/private/module-deployments/schema-transition-'.$manifestHash.'.json',json_encode($receipt,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo json_encode($result+['activated'=>false,'rights_granted'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR);
}catch(Throwable $e){fwrite(STDERR,$e->getMessage()."\n");exit(1);}
+54
View File
@@ -0,0 +1,54 @@
import fs from 'node:fs';import path from 'node:path';import crypto from 'node:crypto';import {spawnSync,spawn} from 'node:child_process';import net from 'node:net';
const repo='C:/laragon/www/compras',parent='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939',source=parent+'/development-app';
const suffix=crypto.randomBytes(6).toString('hex'),database='sgf_sourcing_test_'+suffix,work=parent+'/private/sourcing-'+suffix,clone=work+'/app';
const pg='C:/Program Files/PostgreSQL/17/bin',php='C:/Users/macbook/.config/investmoz/php-8.5.11/php.exe';fs.mkdirSync(work,{recursive:true});
const envText=fs.readFileSync(source+'/.env','utf8');const env=Object.fromEntries([...envText.matchAll(/^([A-Z_]+)=(.*)$/gm)].map(m=>[m[1],m[2].trim().replace(/^['"]|['"]$/g,'')]));
if(env.DB_DATABASE!=='sgf_modules_dev_2576427d1939'||env.DB_PORT!=='55439')throw Error('Wrong demonstration');
const admin=fs.readFileSync(parent+'/private/admin-password.txt','utf8');
function run(exe,args,options={}){const r=spawnSync(exe,args,{windowsHide:true,encoding:'utf8',maxBuffer:16*1024*1024,...options});if(r.status!==0)throw Error((r.stdout??'')+(r.stderr??''));return r.stdout;}
const adminEnv={...process.env,PGPASSWORD:admin};const qaEnv={...process.env,PGPASSWORD:env.DB_PASSWORD};
const fingerprintSQL="BEGIN READ ONLY;SELECT md5(coalesce(jsonb_agg(to_jsonb(t) ORDER BY id)::text,'')) FROM ir_requests t;SELECT md5(coalesce(jsonb_agg(to_jsonb(t) ORDER BY id)::text,'')) FROM cb_budget_entries t;SELECT md5(coalesce(jsonb_agg(to_jsonb(t) ORDER BY id)::text,'')) FROM users t;SELECT md5(coalesce(jsonb_agg(to_jsonb(t) ORDER BY id)::text,'')) FROM permission_grants t;COMMIT;";
const sourceFingerprint=(db=env.DB_DATABASE)=>run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',db,'-At','-v','ON_ERROR_STOP=1','-c',fingerprintSQL],{env:qaEnv});
const before=sourceFingerprint();let created=false,server;
try{
const archivedBaseline=parent+'/private/sourcing-recovery/before.dump',useArchivedBaseline=fs.existsSync(archivedBaseline);
if(useArchivedBaseline)fs.copyFileSync(archivedBaseline,work+'/before.dump');else run(pg+'/pg_dump.exe',['-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',env.DB_DATABASE,'-Fc','-f',work+'/before.dump'],{env:qaEnv});
const cluster=run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U','sgf_regression_owner','-d','postgres','-At','-c',"select current_setting('cluster_name')"],{env:adminEnv}).trim();if(cluster!=='sgf_modular_regression_2576427d1939')throw Error('Wrong cluster');
run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U','sgf_regression_owner','-d','postgres','-v','ON_ERROR_STOP=1','-c',`CREATE DATABASE ${database} OWNER sgf_modules_qa_2576427d1939`],{env:adminEnv});created=true;
run(pg+'/pg_restore.exe',['-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',database,'--no-owner','--no-privileges','--exit-on-error',work+'/before.dump'],{env:qaEnv});
const copyBaselineFingerprint=sourceFingerprint(database);
fs.mkdirSync(clone,{recursive:true});
for(const dir of ['app','config','routes','bootstrap','scripts','packages','resources','public'])fs.cpSync(source+'/'+dir,clone+'/'+dir,{recursive:true});
const guard=clone+'/app/Shared/Testing/TestingDatabaseGuard.php';fs.writeFileSync(guard,fs.readFileSync(guard,'utf8').replaceAll('sgf_modules_dev_2576427d1939',database)); // Exact physical identity adaptation; guards remain enabled.
for(const file of ['artisan','composer.json','composer.lock'])fs.copyFileSync(source+'/'+file,clone+'/'+file);fs.cpSync(source+'/storage/app/private/module-deployments',clone+'/storage/app/private/module-deployments',{recursive:true});
for(const d of ['storage/framework/sessions','storage/framework/views','storage/framework/cache/data','storage/logs'])fs.mkdirSync(clone+'/'+d,{recursive:true});
fs.cpSync(source+'/vendor',clone+'/vendor',{recursive:true}); // Independent autoloader and dependencies; never mutate the demo vendor.
for(const dir of fs.readdirSync(clone+'/storage/app/private/module-deployments',{withFileTypes:true})){if(!dir.isDirectory())continue;const file=clone+'/storage/app/private/module-deployments/'+dir.name+'/journal.json';if(fs.existsSync(file)){const j=JSON.parse(fs.readFileSync(file));j.host_identity=crypto.createHash('sha256').update(clone).digest('hex');fs.writeFileSync(file,JSON.stringify(j,null,2));}}
if(useArchivedBaseline){
const expectedBaseline=JSON.parse(fs.readFileSync(repo+'/docs/proofs/sgf-read-candidate.json','utf8')).archive.manifest_sha256;let hops=0;fs.writeFileSync(clone+'/storage/framework/down','{}');
while(true){const current=JSON.parse(fs.readFileSync(clone+'/storage/app/private/module-deployments/current.json','utf8'));if(current.manifest_sha256===expectedBaseline)break;if(++hops>4)throw Error('Unexpected baseline ancestry');const rolled=JSON.parse(run(php,[repo+'/scripts/sgf-code-deploy.php','rollback',clone,current.deployment_id,'Disposable reproduction of pre-sourcing baseline','--offline'],{cwd:clone}));if(rolled.state!=='rolled-back')throw Error('Failed to reproduce prior code');}fs.unlinkSync(clone+'/storage/framework/down');
}
fs.writeFileSync(clone+'/.env',envText.replace(/^DB_DATABASE=.*$/m,'DB_DATABASE='+database).replace(/^APP_ENV=.*$/m,'APP_ENV=testing').replace(/^APP_DEBUG=.*$/m,'APP_DEBUG=false'));
const vars={...process.env,SOURCING_CLONE:clone,SOURCING_DB:database,SOURCING_WORK:work,SOURCING_REPO:repo,APP_BASE_PATH:clone};
const setup=run(php,[repo+'/tests/Support/sgf-sourcing-copy.php','setup'],{cwd:clone,env:vars});
fs.writeFileSync(work+'/setup-output.txt',setup);let receipt;try{receipt=JSON.parse(setup);}catch{throw Error('Setup failed; private diagnostic '+work+'/setup-output.txt');}if(!receipt.passed)throw Error('Setup proof did not pass');
const listener=net.createServer();await new Promise(r=>listener.listen(0,'127.0.0.1',r));const port=listener.address().port;await new Promise(r=>listener.close(r));
server=spawn(php,['-S','127.0.0.1:'+port,'-t','public','scripts/router.php'],{cwd:clone,env:vars,windowsHide:true,stdio:['ignore',fs.openSync(work+'/web-out.log','a'),fs.openSync(work+'/web-error.log','a')]});
await new Promise((resolve,reject)=>{let tries=0;const poll=()=>{const s=net.connect(port,'127.0.0.1');s.once('connect',()=>{s.destroy();resolve();});s.once('error',()=>{s.destroy();if(++tries>50)reject(Error('Test web host unavailable'));else setTimeout(poll,100);});};poll();});
const http=run(process.execPath,[repo+'/tests/Support/sgf-sourcing-http.mjs'],{cwd:repo,env:{...vars,SOURCING_PORT:String(port)}});fs.writeFileSync(work+'/http-output.txt',http);
const result=JSON.parse(http);if(!result.passed)throw Error('HTTP test failed');
server.kill();await new Promise(r=>setTimeout(r,200));server=null;
const recovered=JSON.parse(run(php,[repo+'/tests/Support/sgf-sourcing-copy.php','verify'],{cwd:clone,env:vars}));
// Data/schema recovery is separate from the successful code rollback above.
run(pg+'/pg_dump.exe',['-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',database,'-Fc','-f',work+'/confirmed-before-recovery.dump'],{env:qaEnv});
run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U','sgf_regression_owner','-d','postgres','-v','ON_ERROR_STOP=1','-c',`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${database}' AND pid<>pg_backend_pid()`,'-c',`DROP DATABASE ${database}`,'-c',`CREATE DATABASE ${database} OWNER sgf_modules_qa_2576427d1939`],{env:adminEnv});
run(pg+'/pg_restore.exe',['-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',database,'--no-owner','--no-privileges','--exit-on-error',work+'/before.dump'],{env:qaEnv});
if(sourceFingerprint(database)!==copyBaselineFingerprint)throw Error('Restored source request, reservation, accounts or grants diverged');
const schemaRestored=run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',database,'-At','-c',"SELECT count(*) FROM pg_tables WHERE schemaname='public' AND tablename LIKE 'ss_%'; SELECT count(*) FROM sgc_installations WHERE module='sourcing'"],{env:qaEnv}).trim();if(schemaRestored!=='0\n0'&&schemaRestored!=='0\r\n0')throw Error('Schema recovery diverged');
recovered.data_schema_restore_verified=true;recovered.pre_recovery_state_archived=true;recovered.restored_requests_reservations_accounts_grants_match=true;
const proof={passed:true,database,work,setup:receipt,http:result,recovery:recovered,persistent_demo_unchanged:before===sourceFingerprint(),synthetic_copy_only:true,checked_at:new Date().toISOString()};if(!proof.persistent_demo_unchanged)throw Error('Original demo changed');
fs.writeFileSync(repo+'/docs/proofs/sgf-sourcing-isolated.json',JSON.stringify(proof,null,2));console.log(JSON.stringify(proof));
}finally{
if(server)server.kill();
if(created)run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U','sgf_regression_owner','-d','postgres','-v','ON_ERROR_STOP=1','-c',`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${database}' AND pid<>pg_backend_pid()`,'-c',`DROP DATABASE ${database}`],{env:adminEnv});
}
+191
View File
@@ -0,0 +1,191 @@
<?php
use Tempo\Purchasing\Delivery\CodeDeployment;
require dirname(__DIR__).'/integrations/sgf/operational/CodeDeployment.php';
require dirname(__DIR__).'/tests/Support/sgf-update-fixture.php';
$root = dirname(__DIR__);
$host = realpath($root.'/../sgf-tempo');
$pointer = file_get_contents($host.'/packages/purchasing/current.json');
$old = json_decode(file_get_contents($root.'/docs/proofs/sgf-code-deploy-delivery.json'), true, 32, JSON_THROW_ON_ERROR)['candidate'];
$rows = [];
exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($root.'/scripts/prepare-sgf-operational.php').' '.escapeshellarg($host), $rows, $exit);
if ($exit !== 0) {
throw new RuntimeException('Preparation failed');
}
$prepared = json_decode(implode("\n", $rows), true, 16, JSON_THROW_ON_ERROR);
$new = $prepared['directory'];
$oldHash = hash_file('sha256', $old.'/manifest.json');
$newHash = hash_file('sha256', $new.'/manifest.json');
$manifest = json_decode(file_get_contents($old.'/manifest.json'), true, 32, JSON_THROW_ON_ERROR);
$copyBundle = function (string $source, string $target) {
$manifest = json_decode(file_get_contents($source.'/manifest.json'), true, 32, JSON_THROW_ON_ERROR);
foreach ([...array_keys($manifest['support_files']), 'manifest.json'] as $file) {
if (! is_dir(dirname($target.'/'.$file))) {
mkdir(dirname($target.'/'.$file), 0700, true);
}copy($source.'/'.$file, $target.'/'.$file);
}
};
$checks = [];
$check = function ($ok, $name) use (&$checks) {
if (! $ok) {
throw new RuntimeException($name);
}$checks[] = $name;
};
$check(hash_file('sha256', $prepared['archive']['file']) === $prepared['archive']['sha256'] && $prepared['archive']['manifest_sha256'] === $newHash, 'portable review ZIP matches approved bundle and manifest');
$deny = function (callable $call, string $text) use ($check) {
try {
$call();
throw new LogicException('Unexpected success');
} catch (RuntimeException $error) {
$check(str_contains($error->getMessage(), $text), $text);
}
};
$apply = function (CodeDeployment $deployment, string $bundle, string $hash, ?callable $checkpoint = null) {
$plan = $deployment->plan($bundle, $hash);
return $deployment->apply($bundle, $hash, $plan['target_sha256'], 'Synthetic version operator', true, $checkpoint);
};
$copy = updateFixture($host, $manifest);
$d = new CodeDeployment($copy);
$v1 = $apply($d, $old, $oldHash);
$unowned = updateFixture($host, $manifest);
$copyBundle($old, $unowned.'/packages/internal-requisitions');
$deny(fn () => (new CodeDeployment($unowned))->plan($new, $newHash), 'sem proveniência');
$mutant = sys_get_temp_dir().'/sgf-update-schema-'.bin2hex(random_bytes(6));
$copyBundle($new, $mutant);
$changed = json_decode(file_get_contents($mutant.'/manifest.json'), true, 32, JSON_THROW_ON_ERROR);
$driver = 'integrations/sgf/operational/Installation.php';
file_put_contents($mutant.'/'.$driver, file_get_contents($mutant.'/'.$driver)."\n// Synthetic schema-driver revision\n");
$changed['support_files'][$driver] = hash_file('sha256', $mutant.'/'.$driver);
file_put_contents($mutant.'/manifest.json', json_encode($changed, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$deny(fn () => $d->plan($mutant, hash_file('sha256', $mutant.'/manifest.json')), 'Transição de schema');
$plan = $d->plan($new, $newHash);
$check($plan['previous']['deployment_id'] === $v1['deployment_id'], 'plan binds update to active installation');
$v2 = $apply($d, $new, $newHash);
$check(hash_file('sha256', $copy.'/packages/internal-requisitions/manifest.json') === $newHash, 'new version applied without source connection');
$check(is_dir($copy.'/storage/app/private/module-deployments/'.$v2['deployment_id'].'/previous-bundle'), 'previous version preserved locally');
$deny(fn () => $d->rollback($v1['deployment_id'], 'Synthetic out-of-order', true), 'fora de ordem');
$deny(fn () => $d->apply($old, $oldHash, $plan['target_sha256'], 'Synthetic stale replay', true), 'versão activa');
$file = array_key_first($manifest['host_overlays']);
$content = file_get_contents($copy.'/'.$file);
file_put_contents($copy.'/'.$file, $content."\n// local work\n");
$deny(fn () => $d->plan($new, $newHash), 'Código aplicado alterado');
file_put_contents($copy.'/'.$file, $content);
file_put_contents($copy.'/packages/internal-requisitions/local-extension.php', '<?php // synthetic unrecorded extension');
$deny(fn () => $d->plan($new, $newHash), 'Conteúdo extra');
unlink($copy.'/packages/internal-requisitions/local-extension.php');
$d->rollback($v2['deployment_id'], 'Synthetic version rollback', true);
$check(hash_file('sha256', $copy.'/packages/internal-requisitions/manifest.json') === $oldHash, 'rollback restores previous bundle exactly');
$check(json_decode(file_get_contents($copy.'/storage/app/private/module-deployments/current.json'), true)['deployment_id'] === $v1['deployment_id'], 'rollback restores active provenance');
$retryPlan = $d->plan($new, $newHash);
$check($retryPlan['deployment_id'] !== $v2['deployment_id'], 'new attempt preserves rolled-back journal');
$deny(fn () => $apply($d, $new, $newHash, function ($step) {
if ($step === 'previous-bundle-preserved') {
throw new RuntimeException('Injected update switch failure');
}
}), 'Injected update switch failure');
$d->rollback($retryPlan['deployment_id'], 'Synthetic interrupted transition', true);
$check(hash_file('sha256', $copy.'/packages/internal-requisitions/manifest.json') === $oldHash, 'interrupted transition recovers previous version');
$again = $apply($d, $new, $newHash);
$check($again['deployment_id'] !== $retryPlan['deployment_id'], 'reapply creates a separate audited attempt');
$d->rollback($again['deployment_id'], 'Synthetic clean transition rollback', true);
$d->rollback($v1['deployment_id'], 'Synthetic first installation rollback', true);
$check(! is_dir($copy.'/packages/internal-requisitions'), 'complete rollback restores original host composition');
$native = updateFixture($host, $manifest, true);
$nativeDeploy = new CodeDeployment($native);
$environment = ['APP_ENV' => 'production', 'APP_DEBUG' => 'false', 'APP_BASE_PATH' => $native, 'APP_KEY' => 'base64:'.base64_encode(random_bytes(32)), 'DB_CONNECTION' => 'sqlite', 'DB_DATABASE' => ':memory:', 'DB_URL' => '', 'CACHE_STORE' => 'array', 'SESSION_DRIVER' => 'file', 'QUEUE_CONNECTION' => 'sync'];
$previousEnv = [];
foreach ($environment as $key => $value) {
$previousEnv[$key] = getenv($key);
putenv($key.'='.$value);
}
$server = null;
$start = function () use ($native) {
$socket = stream_socket_server('tcp://127.0.0.1:0');
$name = stream_socket_get_name($socket, false);
fclose($socket);
$port = (int) substr(strrchr($name, ':'), 1);
$process = proc_open([PHP_BINARY, '-S', '127.0.0.1:'.$port, '-t', $native.'/public'], [0 => ['pipe', 'r'], 1 => ['file', $native.'/storage/logs/server.log', 'a'], 2 => ['file', $native.'/storage/logs/server.log', 'a']], $pipes, $native);
fclose($pipes[0]);
for ($i = 0; $i < 100; $i++) {
$probe = @fsockopen('127.0.0.1', $port);
if ($probe) {
fclose($probe);
break;
}usleep(50000);
}
return [$process, $port];
};
$stop = function ($process) {
proc_terminate($process);
proc_close($process);
};
$status = function ($port, $path) {
$context = stream_context_create(['http' => ['ignore_errors' => true, 'header' => "Accept: application/json\r\n", 'timeout' => 10]]);
file_get_contents('http://127.0.0.1:'.$port.$path, false, $context);
preg_match('/\s([0-9]{3})\s/', $http_response_header[0], $match);
return (int) $match[1];
};
try {
$a = $apply($nativeDeploy, $old, $oldHash);
$nativeDeploy->rebuildCaches($a['deployment_id'], 'Synthetic native cache V1', true);
$check(is_file($native.'/bootstrap/cache/config.php') && is_file($native.'/bootstrap/cache/routes-v7.php') && is_file($native.'/bootstrap/cache/events.php'), 'native Laravel config route and event caches built');
$b = $apply($nativeDeploy, $new, $newHash);
$check(! is_file($native.'/bootstrap/cache/config.php') && ! is_file($native.'/bootstrap/cache/routes-v7.php'), 'version update invalidates previous native caches');
$deny(fn () => $nativeDeploy->rebuildCaches($b['deployment_id'], 'Synthetic cache compilation failure', true, function ($command) {
if ($command === 'config:cache') {
throw new RuntimeException('Injected cache compilation interruption');
}
}), 'Injected cache compilation interruption');
$cacheJournal = json_decode(file_get_contents($native.'/storage/app/private/module-deployments/'.$b['deployment_id'].'/journal.json'), true, 32, JSON_THROW_ON_ERROR);
$check($cacheJournal['cache_state'] === 'failed' && is_file($native.'/storage/framework/down'), 'partial native cache build is journalled under maintenance');
file_put_contents($native.'/bootstrap/cache/config.php', '<?php invalid_partial_cache(');
$cliRows = [];
$cliArgs = [PHP_BINARY, $new.'/scripts/sgf-code-deploy.php', 'cache', $native, $b['deployment_id'], 'Synthetic native cache V2 CLI', '--offline'];
exec(implode(' ', array_map('escapeshellarg', $cliArgs)), $cliRows, $cliExit);
$check($cliExit === 0 && json_decode(implode("\n", $cliRows), true, 16, JSON_THROW_ON_ERROR)['cache_state'] === 'built', 'cache CLI recovers interrupted native compilation');
$check(! str_contains(file_get_contents($native.'/bootstrap/cache/config.php'), 'invalid_partial_cache'), 'invalid partial config cache cannot block native recovery');
[$server,$port] = $start();
$check($status($port, '/purchasing/internal-requisitions') === 503, 'cached deployment remains in native maintenance');
$stop($server);
$server = null;
unlink($native.'/storage/framework/down');
[$server,$port] = $start();
$check($status($port, '/purchasing/internal-requisitions') === 401, 'cached module route survives restart and denies anonymous access');
$stop($server);
$server = null;
file_put_contents($native.'/storage/framework/down', '{"status":503}');
$compiled = file_get_contents($native.'/bootstrap/cache/config.php');
file_put_contents($native.'/bootstrap/cache/config.php', $compiled."\n// subsequent local cache change\n");
$deny(fn () => $nativeDeploy->rollback($b['deployment_id'], 'Synthetic conflicting cache rollback', true), 'Cache mudou após compilação');
file_put_contents($native.'/bootstrap/cache/config.php', $compiled);
$nativeDeploy->rollback($b['deployment_id'], 'Synthetic cached version rollback', true);
$check(! is_file($native.'/bootstrap/cache/config.php') && ! is_file($native.'/bootstrap/cache/routes-v7.php'), 'rollback invalidates registered rebuilt caches');
$nativeDeploy->rebuildCaches($a['deployment_id'], 'Synthetic native cache restored V1', true);
$previousCacheHash = hash_file('sha256', $native.'/bootstrap/cache/config.php');
$replayedRollback = $nativeDeploy->rollback($b['deployment_id'], 'Synthetic repeated version rollback', true);
$check($replayedRollback['replayed'] && hash_file('sha256', $native.'/bootstrap/cache/config.php') === $previousCacheHash, 'repeated rollback preserves previous-version rebuilt caches');
[$server,$port] = $start();
$check($status($port, '/purchasing/internal-requisitions') === 503, 'previous version restarts with native caches and maintenance');
$stop($server);
$server = null;
$nativeDeploy->rollback($a['deployment_id'], 'Synthetic native baseline rollback', true);
unlink($native.'/storage/framework/down');
[$server,$port] = $start();
$check($status($port, '/purchasing/internal-requisitions') === 404, 'restarted baseline exposes no removed module route');
$check($status($port, '/up') === 200, 'baseline starts after cached deployment rollback');
} finally {
if (is_resource($server)) {
$stop($server);
}foreach ($previousEnv as $key => $value) {
putenv($value === false ? $key : $key.'='.$value);
}
}
$check(file_get_contents($host.'/packages/purchasing/current.json') === $pointer, 'destination remains untouched on 0.1.1');
$proof = ['passed' => true, 'checks' => $checks, 'old_candidate' => $old, 'new_candidate' => $new, 'archive' => $prepared['archive'], 'copies' => [$copy, $native, $unowned, $mutant], 'checked_at' => gmdate('c'), 'destination_version' => '0.1.1', 'destination_applied' => false, 'prior_suites_repeated' => false, 'actual_destination_services_touched' => false, 'native_caches_built' => true, 'isolated_server_restarts_verified' => true];
file_put_contents($root.'/docs/proofs/sgf-update-cache-delivery.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)."\n";
+17 -1
View File
@@ -35,7 +35,9 @@ $contextsInstall = in_array('--contexts', $argv ?? [], true);
$noticesInstall = in_array('--notices', $argv ?? [], true);
$warehouseInstall = in_array('--warehouse-drafts', $argv ?? [], true);
$fulfilmentInstall = in_array('--warehouse-events', $argv ?? [], true);
if ((int) $lotInstall + (int) $countInstall + (int) $contextsInstall + (int) $noticesInstall + (int) $warehouseInstall + (int) $fulfilmentInstall > 1) {
$reservationInstall = in_array('--warehouse-reservations', $argv ?? [], true);
$recoveryInstall = in_array('--replenishment-recovery', $argv ?? [], true);
if ((int) $lotInstall + (int) $countInstall + (int) $contextsInstall + (int) $noticesInstall + (int) $warehouseInstall + (int) $fulfilmentInstall + (int) $reservationInstall + (int) $recoveryInstall > 1) {
throw new LogicException('Seleccione uma única migração por instalação.');
}
$migration = $contextsInstall ? '2026_11_01_000045_add_work_context_configuration' : ($countInstall ? '2026_11_01_000044_allow_signed_lot_adjustments' : ($lotInstall ? '2026_11_01_000043_create_inventory_lot_allocations' : '2026_11_01_000042_create_supply_records'));
@@ -52,6 +54,14 @@ if ($fulfilmentInstall) {
$migration = '2026_11_01_000048_create_warehouse_request_events';
$newTable = 'warehouse_request_events';
}
if ($reservationInstall) {
$migration = '2026_11_02_000049_create_warehouse_reservations';
$newTable = 'warehouse_reservations';
}
if ($recoveryInstall) {
$migration = '2026_11_02_000050_create_replenishment_work';
$newTable = 'replenishment_work';
}
if ($connection->table('migrations')->where('migration', $migration)->exists()) {
echo "A migração de fornecimento já está instalada; nenhum registo alterado.\n";
exit;
@@ -115,6 +125,12 @@ try {
$proof['commercial_policy_activated'] = false;
}
$proofName = $fulfilmentInstall ? 'warehouse-fulfilment-install' : ($warehouseInstall ? 'warehouse-drafts-install' : ($noticesInstall ? 'workflow-notices-install' : ($contextsInstall ? 'work-context-install' : ($countInstall ? 'inventory-count-install' : ($lotInstall ? 'inventory-lot-install' : 'supply-install')))));
if ($reservationInstall) {
$proofName = 'warehouse-reservations-install';
}
if ($recoveryInstall) {
$proofName = 'replenishment-recovery-install';
}
file_put_contents(dirname(__DIR__).'/docs/proofs/'.$proofName.'.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo 'Migração dirigida instalada; restauro conferido, '.count($preserved)." tabelas preservadas. Sem atribuições ou dados de negócio.\n";
} finally {
+32
View File
@@ -0,0 +1,32 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const viewer = client();
await viewer.enter('admin');
const dashboard = await viewer.request('/dashboard');
if (dashboard.page?.props.context?.enforced) {
await viewer.request('/contexto', 'POST', { area: 'procurement', option: 'procurement.internal' });
}
const first = await viewer.request('/stock/integracao/entregas?limit=1', 'GET', undefined, false);
assert.equal(first.status, 200);
const data = JSON.parse(first.text);
assert.equal(data.schema, 'sgc.warehouse.fulfilment-feed.v1');
assert.ok(Array.isArray(data.items) && data.items.length <= 1);
assert.equal(typeof data.has_more, 'boolean');
assert.equal(typeof data.next_cursor, 'string');
const next = await viewer.request('/stock/integracao/entregas?cursor=' + encodeURIComponent(data.next_cursor), 'GET', undefined, false);
assert.equal(next.status, 200);
assert.equal(JSON.parse(next.text).schema, data.schema);
const invalid = await viewer.request('/stock/integracao/entregas?limit=101');
assert.equal(invalid.status, 422);
await fs.writeFile('docs/proofs/warehouse-exchange-http.json', JSON.stringify({
checked_at: new Date().toISOString(), environment: base, result: 'passed',
cursor_roundtrip: true, limit_validated: true, observed_first_page_items: data.items.length,
business_commands: false, accounts_or_grants_changed: false,
sgf_installation_changed: false, visual_review: false
}, null, 2));
console.log('Consulta de integração do armazém e cursor conferidos em HTTP.');
` + '})()')(fs, assert);
+45
View File
@@ -0,0 +1,45 @@
<?php
use Tempo\Integration\WarehouseHttpSource;
use Tempo\Integration\WarehouseInbox;
use Tempo\Integration\WarehousePull;
use Tempo\Integration\WarehouseTransportFailure;
require __DIR__.'/../packages/integration/bootstrap.php';
// Read-only QA origin; receiver tables live only in this disposable memory store.
try {
$input = json_decode(stream_get_contents(STDIN), true, 16, JSON_THROW_ON_ERROR);
if (! is_array($input) || ! is_string($input['cookie'] ?? null) || ! is_int($input['tenant_id'] ?? null) || $input['tenant_id'] <= 0) {
throw new RuntimeException('Entrada de avaliação inválida.');
}
$db = new PDO('sqlite::memory:');
foreach (WarehouseInbox::schema() as $sql) {
$db->exec($sql);
}
$source = new WarehouseHttpSource('http://127.0.0.1:8005', $input['tenant_id'], fn () => $input['cookie'], allowLoopbackHttp: true);
$inbox = new WarehouseInbox($db, 'synthetic-qa-8005', $input['tenant_id'], 99, 'disposable-evaluation');
$result = (new WarehousePull($inbox))->run($source, maxPages: 1, pageSize: 1);
$checkpoint = $inbox->checkpoint();
$next = $source($checkpoint['cursor'], 1);
if ($next['schema'] !== 'sgc.warehouse.fulfilment-feed.v1') {
throw new RuntimeException('Contrato de avaliação inesperado.');
}
$anonymousDenied = false;
try {
$anonymous = new WarehouseHttpSource('http://127.0.0.1:8005', $input['tenant_id'], fn () => 'SGC_TEST_SESSION=not-authenticated', allowLoopbackHttp: true);
$anonymous(null, 1);
} catch (WarehouseTransportFailure $e) {
$anonymousDenied = $e->category === 'authentication';
}
if (! $anonymousDenied) {
throw new RuntimeException('Acesso anónimo não recusado como esperado.');
}
echo json_encode(['result' => 'passed', 'transport' => 'native PHP cURL to existing QA session', 'pages' => $result['pages'],
'received_facts' => $result['inserted'], 'receiver_checkpoint_version' => $checkpoint['version'],
'cursor_roundtrip' => true, 'unauthenticated_read_denied' => true, 'receiver' => 'disposable SQLite memory',
'persistent_business_commands' => false, 'persistent_accounts_or_grants_changed' => false, 'sgf_installation_changed' => false], JSON_THROW_ON_ERROR);
} catch (Throwable) {
fwrite(STDERR, 'A avaliação HTTP do conector não foi aprovada. Nenhuma credencial é apresentada.'.PHP_EOL);
exit(1);
}
+42
View File
@@ -0,0 +1,42 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const original = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
const common = original.replace(' request,', ` request,
cookieHeader: () => [...cookies].map(([k, v]) => k + '=' + v).join('; '),`);
assert.notEqual(common, original, 'Expected existing QA cookie helper');
await new Function('fs', 'assert', 'spawn', 'return (async()=>{' + common + `
const viewer = client();
await viewer.enter('admin');
const dashboard = await viewer.request('/dashboard');
if (dashboard.page?.props.context?.enforced || dashboard.page?.props.workContext?.enforced) {
await viewer.request('/contexto', 'POST', { area: 'procurement', option: 'procurement.internal' });
}
const tenant = dashboard.page?.props.auth?.user?.tenant_id;
assert.ok(Number.isSafeInteger(tenant) && tenant > 0);
const result = await new Promise((resolve, reject) => {
const child = spawn('php', ['scripts/warehouse-http-evaluation.php'], {
cwd: process.cwd(), windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'], timeout: 30000
});
let output = '';
child.stdout.on('data', chunk => { output += chunk; });
child.stderr.resume();
child.on('error', () => reject(new Error('Não foi possível iniciar o ensaio do conector.')));
child.on('close', code => {
if (code !== 0) return reject(new Error('A avaliação HTTP do conector não foi aprovada.'));
try { resolve(JSON.parse(output)); } catch { reject(new Error('Resultado de avaliação inválido.')); }
});
// Session travels only over the private child stdin, never in arguments or proof files.
child.stdin.end(JSON.stringify({ tenant_id: tenant, cookie: viewer.cookieHeader() }));
});
assert.equal(result.result, 'passed');
assert.equal(result.unauthenticated_read_denied, true);
await fs.writeFile('docs/proofs/warehouse-http-source-http.json', JSON.stringify({
checked_at: new Date().toISOString(), environment: base, ...result, credentials_recorded: false,
technical_credential_issued: false, visual_review: false
}, null, 2));
console.log('Cliente HTTP autenticado e receptor descartável conferidos contra a avaliação8005.');
` + '})()')(fs, assert, spawn);
@@ -0,0 +1,13 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const viewer=client();await viewer.enter('admin');
const dashboard=await viewer.request('/dashboard');
if(dashboard.page?.props.context?.enforced)await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'});
const response=await viewer.request('/stock/piloto');assert.equal(response.status,200);assert.equal(response.page.component,'stock/pilot');
const p=response.page.props;assert.equal(p.syntheticEvaluation,true);assert.ok(p.report.checks.length>=6);assert.ok(Array.isArray(p.report.registered));assert.ok(Array.isArray(p.report.participants.prepare));assert.equal(typeof p.report.warehouse_scenario_ready,'boolean');
await fs.writeFile('docs/proofs/warehouse-pilot-readiness-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,component:'stock/pilot',synthetic_evaluation:true,registered_count:p.report.registered.length,warehouse_scenario_ready:p.report.warehouse_scenario_ready,replenishment_scenario_ready:p.report.replenishment_scenario_ready,client_configuration_complete:p.report.client_configuration_complete,checks:p.report.checks.map(c=>({key:c.key,ok:c.ok})),accounts_or_grants_changed:false,business_commands:false,visual_browser_review:false},null,2));
console.log('Diagnóstico do piloto conferido por HTTP sem alterar configurações ou executar compras.');
`+'})()')(fs,assert);
+17
View File
@@ -0,0 +1,17 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const viewer=client();await viewer.enter('admin');
const dashboard=await viewer.request('/dashboard');
if(dashboard.page?.props.context?.enforced)await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'});
const response=await viewer.request('/stock/requisicoes');assert.equal(response.status,200);
const p=response.page.props;assert.equal(response.page.component,'stock/requests');assert.equal(p.ready,true);assert.equal(p.canPrepare,false);
assert.ok(p.views.some(v=>v.key==='all'));assert.ok(Array.isArray(p.records.data));assert.equal(typeof p.records.total,'number');assert.ok(p.checkedAt);
for(const view of ['review','reserve','delivery','receipt']){const r=await viewer.request('/stock/requisicoes?view='+view);assert.equal(r.status,200);assert.equal(r.page.props.records.total,0);}
const settings=await viewer.request('/administracao/parametrizacao');assert.equal(settings.status,200);
assert.ok(settings.page.props.workflowTasks.warehouse_request_prepare);assert.ok(settings.page.props.workflowTasks.warehouse_request_review);assert.ok(settings.page.props.workflowTasks.warehouse_request_deliver);
await fs.writeFile('docs/proofs/warehouse-workdesk-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,component:'stock/requests',read_only_administrator:true,task_views_enforce_permissions:true,filters_and_pagination:true,profile_tasks_present:true,accounts_or_grants_changed:false,business_commands:false,visual_browser_review:false},null,2));
console.log('Acompanhamento de armazém: consulta, tarefas por competência e paginação conferidos em HTTP.');
`+'})()')(fs,assert);
+151 -4
View File
@@ -18,21 +18,76 @@ config(['database.connections.context_proof_admin' => [...$config, 'database' =>
$admin = DB::connection('context_proof_admin');
$admin->statement('CREATE DATABASE "'.$database.'"');
$workflow = in_array('--workflow', $argv ?? [], true);
$warehouse = in_array('--warehouse', $argv ?? [], true);
$pilot = in_array('--pilot', $argv ?? [], true);
$workdesk = in_array('--workdesk', $argv ?? [], true);
$readiness = in_array('--readiness', $argv ?? [], true);
$exchange = in_array('--exchange', $argv ?? [], true);
$transport = in_array('--transport', $argv ?? [], true);
$internalTrace = in_array('--internal-trace', $argv ?? [], true);
$taskInbox = in_array('--task-inbox', $argv ?? [], true);
$financeIntegration = in_array('--finance-integration', $argv ?? [], true);
$awardTasks = in_array('--award-tasks', $argv ?? [], true);
$quotationTasks = in_array('--quotation-tasks', $argv ?? [], true);
$organizationPreview = in_array('--organization-preview', $argv ?? [], true);
$pilotAccessProposal = in_array('--pilot-access-proposal', $argv ?? [], true);
$formalTasks = in_array('--formal-tasks', $argv ?? [], true);
$inbox = in_array('--inbox', $argv ?? [], true) || $transport;
$warehouse = in_array('--warehouse', $argv ?? [], true) || $pilot || $workdesk || $readiness;
try {
$environment = ['APP_ENV' => 'testing', 'DB_CONNECTION' => 'pgsql', 'DB_URL' => '', 'DB_HOST' => $config['host'], 'DB_PORT' => (string) $config['port'],
'DB_DATABASE' => $database, 'DB_USERNAME' => $config['username'], 'DB_PASSWORD' => $config['password']];
$tests = $workflow ? ['tests/Feature/InternalRequisitionCandidateTest.php', 'tests/Feature/WorkContextTest.php'] : ['tests/Feature/WorkContextTest.php'];
if ($warehouse) {
$tests = ['tests/Feature/WarehouseRequestsTest.php', 'tests/Feature/WarehouseAvailabilityTest.php', 'tests/Feature/WarehouseFulfilmentTest.php'];
$tests = ['tests/Feature/WarehouseRequestsTest.php', 'tests/Feature/WarehouseAvailabilityTest.php', 'tests/Feature/WarehouseFulfilmentTest.php', 'tests/Feature/AutomaticPurchasesTest.php'];
}
$process = new Process([PHP_BINARY, 'vendor/bin/pest', '--compact', ...$tests], dirname(__DIR__), $environment, timeout: 120);
if ($readiness) {
$tests = [...$tests, 'tests/Feature/WarehousePilotReadinessTest.php', 'tests/Feature/WarehousePilotJourneyTest.php'];
}
if ($exchange) {
$tests = ['tests/Feature/WarehouseExchangeTest.php'];
}
if ($inbox) {
$environment['INBOX_TEST_PG'] = '1';
$tests = ['tests/Unit/WarehouseInboxTest.php', 'tests/Feature/WarehouseExchangeTest.php'];
}
if ($transport) {
$tests[] = 'tests/Unit/WarehouseHttpSourceTest.php';
}
if ($internalTrace) {
$tests = ['tests/Feature/InternalSupplyTraceTest.php', 'tests/Feature/WarehousePilotJourneyTest.php'];
}
if ($taskInbox) {
$tests = ['tests/Feature/WorkflowTaskInboxTest.php', 'tests/Feature/InternalSupplyTraceTest.php', 'tests/Feature/WarehousePilotJourneyTest.php'];
}
if ($financeIntegration) {
$tests = ['tests/Feature/FinanceIntegrationTest.php', 'tests/Feature/PaymentIntegrationTest.php', 'tests/Feature/InvoiceTest.php', 'tests/Feature/ExternalPaymentTest.php', 'tests/Feature/PurchaseProcessTest.php', 'tests/Feature/PayablePaymentDecisionTest.php'];
}
if ($awardTasks) {
$tests = ['tests/Feature/PurchaseAwardOrderTest.php'];
}
if ($quotationTasks) {
$tests = ['tests/Feature/SupplierQuotationTest.php', 'tests/Feature/PurchaseAwardOrderTest.php'];
}
if ($organizationPreview) {
$tests = ['tests/Feature/OrganizationActivationPreviewTest.php', 'tests/Feature/WorkContextTest.php'];
}
if ($pilotAccessProposal) {
$tests = ['tests/Feature/PilotAccessProposalTest.php'];
}
if ($formalTasks) {
$tests = ['tests/Feature/FormalAdmissionTest.php', 'tests/Feature/FormalRecommendationTest.php', 'tests/Feature/DocumentaryQualificationTest.php'];
}
$process = new Process([PHP_BINARY, 'vendor/bin/pest', '--compact', ...$tests, ...($formalTasks ? ['--filter=formal task'] : ($quotationTasks ? ['--filter=quotation tasks'] : ($awardTasks ? ['--filter=task inbox'] : [])))], dirname(__DIR__), $environment, timeout: $financeIntegration || $awardTasks || $quotationTasks || $organizationPreview || $pilotAccessProposal || $formalTasks ? 300 : 120);
$process->run();
echo $process->getOutput();
if (! $process->isSuccessful()) {
throw new RuntimeException('Prova PostgreSQL de contexto não aprovada.');
}
$races = [];
if ($inbox) {
require __DIR__.'/../tests/Support/inbox-pg-races.php';
$races = inboxPgRaces($environment, $config, $database);
}
if ($warehouse) {
config(['database.connections.warehouse_proof_read' => [...$config, 'database' => $database, 'url' => null]]);
$reader = DB::connection('warehouse_proof_read');
@@ -75,9 +130,101 @@ try {
}
$races[] = ['scenario' => $scenario, 'real_lock_contention' => true, 'result' => 'passed'];
}
$fixture = new Process([PHP_BINARY, 'tests/Concurrency/replenishment-worker.php', 'prepare'], dirname(__DIR__), $environment, timeout: 30);
$fixture->mustRun();
$data = json_decode($fixture->getOutput(), true, 32, JSON_THROW_ON_ERROR);
$first = new Process([PHP_BINARY, 'tests/Concurrency/replenishment-worker.php', 'hold', json_encode($data, JSON_THROW_ON_ERROR)], dirname(__DIR__), $environment, timeout: 30);
$first->start();
$deadline = microtime(true) + 5;
while ($first->isRunning() && ! str_contains($first->getOutput(), 'locked') && microtime(true) < $deadline) {
usleep(20000);
}
if (! str_contains($first->getOutput(), 'locked')) {
throw new RuntimeException('Falta bloqueio da compra automática.');
}
$second = new Process([PHP_BINARY, 'tests/Concurrency/replenishment-worker.php', 'run', json_encode($data, JSON_THROW_ON_ERROR)], dirname(__DIR__), $environment, timeout: 30);
$second->start();
$blocked = false;
$deadline = microtime(true) + 5;
while ($first->isRunning() && microtime(true) < $deadline) {
$blocked = (bool) $reader->selectOne("select exists(select 1 from pg_stat_activity where datname=? and wait_event_type='Lock') as blocked", [$database])->blocked;
if ($blocked) {
break;
}usleep(20000);
}
$first->wait();
$second->wait();
if (! $first->isSuccessful() || ! $second->isSuccessful()) {
throw new RuntimeException('Falha na activação concorrente: '.$first->getErrorOutput().$second->getErrorOutput());
}
$a = json_decode(trim(str_replace('locked', '', $first->getOutput())), true, 32, JSON_THROW_ON_ERROR);
$b = json_decode(trim($second->getOutput()), true, 32, JSON_THROW_ON_ERROR);
if (! $blocked || $a['rule'] !== $b['rule'] || count($a['purchases']) !== 1 || $a['purchases'] !== $b['purchases'] || (int) $reader->table('requisitions')->where('tenant_id', $data['tenant'])->where('status', 'draft')->count() !== 1) {
throw new RuntimeException('A reposição concorrente duplicou o rascunho.');
}
$races[] = ['scenario' => 'automatic_purchase_activation_retry', 'real_lock_contention' => true, 'single_draft' => true, 'result' => 'passed'];
if ($pilot) {
$fixture = new Process([PHP_BINARY, 'tests/Concurrency/recovery-worker.php', 'prepare'], dirname(__DIR__), $environment, timeout: 30);
$fixture->mustRun();
$data = json_decode($fixture->getOutput(), true, 32, JSON_THROW_ON_ERROR);
$before = $reader->table('replenishment_work')->where('id', $data['work'])->sole();
if ($before->generation <= $before->processed_generation || $reader->table('requisitions')->where('tenant_id', $data['tenant'])->where('status', 'draft')->exists()) {
throw new RuntimeException('A prova não deixou sinal durável sem rascunho após interrupção.');
}
$first = new Process([PHP_BINARY, 'tests/Concurrency/recovery-worker.php', 'hold', json_encode($data, JSON_THROW_ON_ERROR)], dirname(__DIR__), $environment, timeout: 30);
$first->start();
$deadline = microtime(true) + 5;
while ($first->isRunning() && ! str_contains($first->getOutput(), 'locked') && microtime(true) < $deadline) {
usleep(20000);
}
if (! str_contains($first->getOutput(), 'locked')) {
throw new RuntimeException('Primeiro recuperador sem bloqueio.');
}
$second = new Process([PHP_BINARY, 'tests/Concurrency/recovery-worker.php', 'run', json_encode($data, JSON_THROW_ON_ERROR)], dirname(__DIR__), $environment, timeout: 30);
$second->start();
$blocked = false;
$deadline = microtime(true) + 5;
while ($first->isRunning() && microtime(true) < $deadline) {
$blocked = (bool) $reader->selectOne("select exists(select 1 from pg_stat_activity where datname=? and wait_event_type='Lock') as blocked", [$database])->blocked;
if ($blocked) {
break;
}usleep(20000);
}
$first->wait();
$second->wait();
if (! $first->isSuccessful() || ! $second->isSuccessful()) {
throw new RuntimeException('Recuperação concorrente falhou.');
}
$a = json_decode(trim(str_replace('locked', '', $first->getOutput())), true, 32, JSON_THROW_ON_ERROR);
$b = json_decode(trim($second->getOutput()), true, 32, JSON_THROW_ON_ERROR);
$after = $reader->table('replenishment_work')->where('id', $data['work'])->sole();
if (! $blocked || $a['result'] !== 'saved' || $b['result'] !== 'saved' || $a['authenticated'] || $b['authenticated'] || count($a['drafts']) !== 1 || $a['drafts'] !== $b['drafts'] || $after->generation !== $after->processed_generation) {
throw new RuntimeException('A recuperação perdeu ou duplicou a compra.');
}
$races[] = ['scenario' => 'abrupt_exit_after_physical_commit_then_two_recovery_workers', 'real_lock_contention' => true, 'single_draft' => true, 'no_authenticated_human' => true, 'result' => 'passed'];
}
DB::purge('warehouse_proof_read');
}
file_put_contents(dirname(__DIR__).'/docs/proofs/'.($warehouse ? 'warehouse-drafts-pg' : ($workflow ? 'workflow-notices-pg' : 'work-context-pg')).'.json', json_encode(['checked_at' => gmdate('c'), 'database' => 'disposable PostgreSQL', 'result' => trim($process->getOutput()), 'races' => $races, 'persistent_data_changed' => false, 'database_removed_in_finally' => true], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$proofName = match (true) {
$formalTasks => 'formal-task-integration-pg',
$pilotAccessProposal => 'pilot-access-proposal-pg',
$organizationPreview => 'organization-activation-preview-pg',
$quotationTasks => 'quotation-task-integration-pg',
$awardTasks => 'award-task-integration-pg',
$financeIntegration => 'finance-integration-pg',
$taskInbox => 'workflow-task-inbox-pg',
$internalTrace => 'internal-supply-integration-pg',
$transport => 'warehouse-http-source-pg',
$inbox => 'warehouse-inbox-pg',
$exchange => 'warehouse-exchange-pg',
$readiness => 'warehouse-pilot-readiness-pg',
$workdesk => 'warehouse-workdesk-pg',
$pilot => 'replenishment-pilot-pg',
$warehouse => 'warehouse-drafts-pg',
$workflow => 'workflow-notices-pg',
default => 'work-context-pg',
};
file_put_contents(dirname(__DIR__).'/docs/proofs/'.$proofName.'.json', json_encode(['checked_at' => gmdate('c'), 'database' => 'disposable PostgreSQL', 'result' => trim($process->getOutput()), 'races' => $races, 'persistent_data_changed' => false, 'database_removed_in_finally' => true], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
} finally {
$admin->select('select pg_terminate_backend(pid) from pg_stat_activity where datname = ? and pid <> pg_backend_pid()', [$database]);
$admin->statement('DROP DATABASE "'.$database.'"');
+33
View File
@@ -0,0 +1,33 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const viewer = client();
await viewer.enter('admin');
const dashboard = await viewer.request('/dashboard');
const context = dashboard.page?.props.workContext ?? dashboard.page?.props.context;
const routes = [];
for (const [area, key, path] of [['procurement', 'procurement.internal', '/requisicoes/tarefas'], ['people', 'people.internal', '/pessoas/tarefas']]) {
if (context?.enforced && !context.available?.includes(key)) {
routes.push({ area, result: 'not_available_in_existing_scope' });
continue;
}
if (context?.enforced) {
assert.equal((await viewer.request('/contexto', 'POST', { area, option: key })).status, 302);
}
const response = await viewer.request(path);
assert.equal(response.status, 200);
assert.equal(response.page.props.area, area);
assert.ok(Array.isArray(response.page.props.tasks.data));
assert.ok(response.page.props.tasks.data.length <= 25);
assert.ok(response.page.props.tasks.total >= response.page.props.tasks.data.length);
routes.push({ area, result: 'passed', total: response.page.props.tasks.total, shown: response.page.props.tasks.data.length });
}
await fs.writeFile('docs/proofs/workflow-task-inbox-http.json', JSON.stringify({
checked_at: new Date().toISOString(), environment: base, routes, existing_account_used: true,
business_commands: false, accounts_or_grants_changed: false, sgf_installation_changed: false, visual_review: false
}, null, 2));
console.log('Listas de tarefas por área conferidas com o âmbito existente.');
` + '})()')(fs, assert);