name: F4 one-shot pre-merge corrections on: push: branches: - feature/f4-backend-saneamiento-documental permissions: contents: write jobs: patch: if: github.actor != 'github-actions[bot]' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: feature/f4-backend-saneamiento-documental - name: Apply audited F4 corrections run: | python - <<'PY' from pathlib import Path def replace_once(path: str, old: str, new: str) -> None: file = Path(path) text = file.read_text() count = text.count(old) if count != 1: raise SystemExit(f"{path}: expected exactly one match, got {count}") file.write_text(text.replace(old, new)) # 1) Physical DB default must match the active F4 report lifecycle. migration = 'api-v3/src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts' replace_once( migration, """ await queryRunner.query(`\n ALTER TABLE inspection_reports\n ADD CONSTRAINT chk_inspection_reports_status CHECK (\n status IN ('WORKING','OFFICIALIZED','FROZEN','CANCELLED')\n )\n `);\n\n await queryRunner.query(`\n ALTER TABLE inspection_acts\n""", """ await queryRunner.query(`\n ALTER TABLE inspection_reports\n ADD CONSTRAINT chk_inspection_reports_status CHECK (\n status IN ('WORKING','OFFICIALIZED','FROZEN','CANCELLED')\n )\n `);\n await queryRunner.query(`\n ALTER TABLE inspection_reports\n ALTER COLUMN status SET DEFAULT 'WORKING'\n `);\n\n await queryRunner.query(`\n ALTER TABLE inspection_acts\n""", ) replace_once( migration, """ await queryRunner.query(`\n UPDATE inspection_reports\n SET status='FROZEN'\n WHERE status IN ('WORKING','OFFICIALIZED')\n `);\n await queryRunner.query(`\n ALTER TABLE inspection_reports\n DROP CONSTRAINT IF EXISTS chk_inspection_reports_status\n""", """ await queryRunner.query(`\n UPDATE inspection_reports\n SET status='FROZEN'\n WHERE status IN ('WORKING','OFFICIALIZED')\n `);\n await queryRunner.query(`\n ALTER TABLE inspection_reports\n ALTER COLUMN status SET DEFAULT 'FROZEN'\n `);\n await queryRunner.query(`\n ALTER TABLE inspection_reports\n DROP CONSTRAINT IF EXISTS chk_inspection_reports_status\n""", ) # 2) A documented company absence is a terminal manifestation: it must not strand a LOCKED Act. closing = 'api-v3/src/inspection-closing/inspection-closing.service.ts' replace_once( closing, """ const companyOutcomes = signatures.filter((item) =>\n item.signerType === InspectionActSignerType.COMPANY_RESPONSIBLE,\n );\n if (companyOutcomes.length !== 1) {\n throw new ConflictException({\n code: 'INSPECTION_ACT_COMPANY_OUTCOME_REQUIRED',\n message: 'Debe existir exactamente una firma, disidencia o negativa del responsable de la empresa',\n });\n }\n const companyOutcome = companyOutcomes[0];\n if (companyOutcome.status === InspectionActSignatureStatus.ABSENT) {\n throw new ConflictException({\n code: 'INSPECTION_ACT_COMPANY_MANIFESTATION_PENDING',\n message: 'La ausencia no reemplaza la firma o negativa; la manifestación de la empresa sigue pendiente',\n });\n }\n\n const serverSealedAt = new Date();\n""", """ const companyOutcomes = signatures.filter((item) =>\n item.signerType === InspectionActSignerType.COMPANY_RESPONSIBLE,\n );\n if (companyOutcomes.length !== 1) {\n throw new ConflictException({\n code: 'INSPECTION_ACT_COMPANY_OUTCOME_REQUIRED',\n message: 'Debe existir exactamente una firma, disidencia, negativa o ausencia documentada del responsable de la empresa',\n });\n }\n\n const serverSealedAt = new Date();\n""", ) # 3) Legacy administrative follow-up remains available, but F4 SEALED Acts must be visible. admin = 'api-v3/src/act-administration/act-administration.service.ts' replace_once( admin, "WHERE ia.status IN ('CLOSED', 'RECTIFIED')", "WHERE ia.status IN ('SEALED', 'CLOSED', 'RECTIFIED')", ) replace_once( admin, "if (!['CLOSED', 'RECTIFIED'].includes(act.status)) throw new BadRequestException({ code: 'ACT_ADMIN_REQUIRES_CLOSED', message: 'El seguimiento administrativo comienza cuando el Acta está cerrada.' });", "if (!['SEALED', 'CLOSED', 'RECTIFIED'].includes(act.status)) throw new BadRequestException({ code: 'ACT_ADMIN_REQUIRES_SEALED', message: 'El seguimiento administrativo comienza cuando el Acta está sellada.' });", ) briefing = 'api-v3/src/act-administration/field-briefing.service.ts' replace_once( briefing, "WHERE act.status IN ('CLOSED', 'RECTIFIED')", "WHERE act.status IN ('SEALED', 'CLOSED', 'RECTIFIED')", ) page = 'web-v2/src/pages/ActAdministrationPage.tsx' replace_once( page, "El plazo y la respuesta de la empresa se gestionan sobre el Acta completa. Los hallazgos quedan dentro de su expediente.", "El plazo y la respuesta de la empresa se gestionan sobre el Acta completa. GEDO no activa el vencimiento por sí solo; el plazo se define cuando corresponda. Los hallazgos quedan dentro de su expediente.", ) # 4) Regression contracts for the three audited pre-merge gaps. test_path = Path('api-v3/test/unit/f4-premerge-review-contract.test.ts') if test_path.exists(): raise SystemExit(f'{test_path}: file already exists') test_path.write_text("""import assert from 'node:assert/strict';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport test from 'node:test';\n\nconst root = process.cwd();\nconst read = (relative: string) => fs.readFileSync(path.join(root, relative), 'utf8');\n\nconst migration = read('src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts');\nconst closing = read('src/inspection-closing/inspection-closing.service.ts');\nconst administration = read('src/act-administration/act-administration.service.ts');\nconst briefing = read('src/act-administration/field-briefing.service.ts');\n\ntest('F4 report status default is WORKING physically and rollback restores FROZEN', () => {\n const up = migration.slice(migration.indexOf(' public async up('), migration.indexOf(' public async down('));\n const down = migration.slice(migration.indexOf(' public async down('));\n assert.match(up, /ALTER COLUMN status SET DEFAULT 'WORKING'/);\n assert.match(down, /ALTER COLUMN status SET DEFAULT 'FROZEN'/);\n});\n\ntest('F4 allows a documented company absence to satisfy the terminal manifestation required for sealing', () => {\n const closeStart = closing.indexOf(' async close(');\n const closeEnd = closing.indexOf(' async signatureContent(', closeStart);\n const close = closing.slice(closeStart, closeEnd);\n assert.match(close, /companyOutcomes\.length !== 1/);\n assert.match(close, /ausencia documentada/);\n assert.doesNotMatch(close, /INSPECTION_ACT_COMPANY_MANIFESTATION_PENDING/);\n assert.doesNotMatch(close, /companyOutcome\.status === InspectionActSignatureStatus\.ABSENT/);\n});\n\ntest('F4 SEALED Acts remain visible in administrative follow-up and field briefing', () => {\n assert.match(administration, /WHERE ia\.status IN \('SEALED', 'CLOSED', 'RECTIFIED'\)/);\n assert.match(administration, /\['SEALED', 'CLOSED', 'RECTIFIED'\]\.includes\(act\.status\)/);\n assert.match(briefing, /WHERE act\.status IN \('SEALED', 'CLOSED', 'RECTIFIED'\)/);\n});\n""") PY git diff --check - name: Commit audited corrections run: | set -Eeuo pipefail if git diff --quiet; then echo 'No changes produced; refusing silent success.' exit 1 fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' git add \ api-v3/src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts \ api-v3/src/inspection-closing/inspection-closing.service.ts \ api-v3/src/act-administration/act-administration.service.ts \ api-v3/src/act-administration/field-briefing.service.ts \ api-v3/test/unit/f4-premerge-review-contract.test.ts \ web-v2/src/pages/ActAdministrationPage.tsx git commit -m 'fix: close F4 pre-merge review gaps' git push origin HEAD:feature/f4-backend-saneamiento-documental