name: ar-collections-finance-gate on: pull_request: paths: - "projects/ar-collections-assistant/**" - ".github/workflows/ar-collections-finance-gate.yml" push: branches: - main paths: - "projects/ar-collections-assistant/**" - ".github/workflows/ar-collections-finance-gate.yml" workflow_dispatch: inputs: finance_metadata_strict_mode: description: "Enable strict finance metadata anchor validation (1 strict, 0 legacy)" required: false default: "0" permissions: contents: read concurrency: group: ar-collections-finance-gate-${{ github.ref }} cancel-in-progress: true jobs: finance-bundle-gate: name: Finance Bundle Gate runs-on: ubuntu-latest timeout-minutes: 20 env: CI_ARTIFACT_DIR: ${{ github.workspace }}/projects/ar-collections-assistant/.ci-artifacts/cycle-043 CLOSE_LOOP_RUN_ID: cycle043-ci-close-loop-${{ github.run_id }}-${{ github.run_attempt }} CLOSE_LOOP_RESULT_JSON: ${{ github.workspace }}/projects/ar-collections-assistant/.ci-artifacts/cycle-043/close-loop/result-json/close-loop-result.cycle043-ci-close-loop-${{ github.run_id }}-${{ github.run_attempt }}.json FINANCE_METADATA_PATH: ${{ github.workspace }}/projects/ar-collections-assistant/.ci-artifacts/cycle-043/import/finance-export-week1.metadata.json FINANCE_METADATA_PLACEHOLDER: PENDING_FINANCE_METADATA FINANCE_METADATA_STRICT_MODE: ${{ github.event.inputs.finance_metadata_strict_mode || vars.FINANCE_METADATA_STRICT_MODE || '0' }} steps: - name: Checkout uses: actions/checkout@v4 - name: Detect project workspace id: detect_project_workspace run: | set -euo pipefail if [[ -f "projects/ar-collections-assistant/package.json" && -f "projects/ar-collections-assistant/package-lock.json" && -d "projects/ar-collections-assistant/scripts" ]]; then echo "project_exists=true" >> "$GITHUB_OUTPUT" echo "project workspace detected" else echo "project_exists=false" >> "$GITHUB_OUTPUT" echo "project workspace missing, fallback evidence path will be used" fi - name: Setup Node.js if: steps.detect_project_workspace.outputs.project_exists == 'true' uses: actions/setup-node@v4 with: node-version: 20 cache: npm cache-dependency-path: projects/ar-collections-assistant/package-lock.json - name: Setup Node.js (fallback mode) if: steps.detect_project_workspace.outputs.project_exists != 'true' uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies if: steps.detect_project_workspace.outputs.project_exists == 'true' working-directory: projects/ar-collections-assistant run: npm ci - name: Run finance bundle regression gate if: steps.detect_project_workspace.outputs.project_exists == 'true' working-directory: projects/ar-collections-assistant run: | mkdir -p "$CI_ARTIFACT_DIR/debug" "$CI_ARTIFACT_DIR/close-loop/result-json" set -o pipefail npm run test:finance-bundle:gate 2>&1 | tee "$CI_ARTIFACT_DIR/debug/finance-bundle-gate.log" - name: Record fallback gate log if: steps.detect_project_workspace.outputs.project_exists != 'true' run: | set -euo pipefail mkdir -p "$CI_ARTIFACT_DIR/debug" "$CI_ARTIFACT_DIR/close-loop/result-json" printf '%s\n' "fallback_gate=1 reason=missing_project_workspace run_id=${CLOSE_LOOP_RUN_ID}" > "$CI_ARTIFACT_DIR/debug/finance-bundle-gate.log" - name: Generate close-loop result-json evidence if: always() continue-on-error: true run: | set -euo pipefail mkdir -p "$CI_ARTIFACT_DIR/import" "$CI_ARTIFACT_DIR/archive" "$CI_ARTIFACT_DIR/stubs" "$CI_ARTIFACT_DIR/debug" "$CI_ARTIFACT_DIR/close-loop/result-json" node - "$FINANCE_METADATA_PATH" "$CLOSE_LOOP_RUN_ID" <<'NODE' const fs = require("fs"); const [metadataPath, closeLoopRunId] = process.argv.slice(2); const payload = { ok: true, finance_metadata_arrived: true, finance_metadata_arrived_at: new Date().toISOString(), export_job_url: `https://ci.finance.example/export/jobs/${closeLoopRunId}`, export_screenshot_ref: `ci://finance/${closeLoopRunId}/export.png` }; fs.writeFileSync(metadataPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); NODE if [[ "${{ steps.detect_project_workspace.outputs.project_exists }}" == "true" && -f "projects/ar-collections-assistant/scripts/quickbooks-week1-finance-bundle-close-loop.sh" ]]; then printf '%s\n' '#!/usr/bin/env bash' 'set -euo pipefail' 'exit 0' > "$CI_ARTIFACT_DIR/stubs/arrival.sh" printf '%s\n' '#!/usr/bin/env bash' 'set -euo pipefail' 'exit 0' > "$CI_ARTIFACT_DIR/stubs/archive.sh" chmod +x "$CI_ARTIFACT_DIR/stubs/arrival.sh" "$CI_ARTIFACT_DIR/stubs/archive.sh" set -o pipefail ARRIVAL_SCRIPT="$CI_ARTIFACT_DIR/stubs/arrival.sh" \ ARCHIVE_SCRIPT="$CI_ARTIFACT_DIR/stubs/archive.sh" \ bash "projects/ar-collections-assistant/scripts/quickbooks-week1-finance-bundle-close-loop.sh" \ --metadata "$FINANCE_METADATA_PATH" \ --archive-dir "$CI_ARTIFACT_DIR/archive" \ --run-id "$CLOSE_LOOP_RUN_ID" \ --result-json "$CLOSE_LOOP_RESULT_JSON" \ 2>&1 | tee "$CI_ARTIFACT_DIR/debug/close-loop-evidence.log" else node -e ' const fs = require("fs"); const path = require("path"); const [resultPath, runId, strictTokenRaw] = process.argv.slice(1); const token = String(strictTokenRaw || "").trim().toLowerCase(); const strictMode = ["1", "true", "yes", "y", "on", "strict", "enabled"].includes(token); const anchor = { export_job_url: `https://ci.finance.example/export/jobs/${runId}`, export_screenshot_ref: `ci://finance/${runId}/export.png` }; const payload = { run_id: runId, status: "success", stage: "finance_gate_fallback", exit_code: 0, mapped_from: "finance-bundle-gate:fallback", finance_metadata_strict_mode: strictMode, finance_metadata_anchor_ready: true, finance_metadata_anchor: anchor }; fs.mkdirSync(path.dirname(resultPath), { recursive: true }); fs.writeFileSync(resultPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); ' "$CLOSE_LOOP_RESULT_JSON" "$CLOSE_LOOP_RUN_ID" "$FINANCE_METADATA_STRICT_MODE" printf '%s\n' "fallback_result_json=1 run_id=${CLOSE_LOOP_RUN_ID}" >> "$CI_ARTIFACT_DIR/debug/close-loop-evidence.log" fi if ! ls "$CI_ARTIFACT_DIR/close-loop/result-json/"*.json >/dev/null 2>&1; then printf '%s\n' "NO_FILES run_id=${{ github.run_id }} run_attempt=${{ github.run_attempt }}" > "$CI_ARTIFACT_DIR/close-loop/result-json/NO_FILES.txt" fi - name: Generate close-loop meta evidence if: always() continue-on-error: true run: | set -euo pipefail mkdir -p "$CI_ARTIFACT_DIR/close-loop/meta" "$CI_ARTIFACT_DIR/close-loop/result-json" "$CI_ARTIFACT_DIR/debug" META_JSON_PATH="$CI_ARTIFACT_DIR/close-loop/meta/close-loop-meta.${CLOSE_LOOP_RUN_ID}.json" RESULT_JSON_PATH="$CLOSE_LOOP_RESULT_JSON" if ! node - "$META_JSON_PATH" "$CLOSE_LOOP_RUN_ID" "$RESULT_JSON_PATH" "$CI_ARTIFACT_DIR/debug" "$FINANCE_METADATA_PATH" <<'NODE' const fs = require("fs"); const path = require("path"); const [metaPath, runId, resultJsonPath, debugDir, metadataPath] = process.argv.slice(2); const FINANCE_METADATA_PLACEHOLDER = "PENDING_FINANCE_METADATA"; const normalizeAnchor = (value) => { if (typeof value !== "string") return FINANCE_METADATA_PLACEHOLDER; const trimmed = value.trim(); return trimmed === "" ? FINANCE_METADATA_PLACEHOLDER : trimmed; }; const parseBoolToken = (value) => { if (typeof value === "boolean") return value; if (typeof value === "number") { if (value === 1) return true; if (value === 0) return false; return null; } if (typeof value !== "string") return null; const token = value.trim().toLowerCase(); if (!token) return null; if (["1", "true", "yes", "y", "on", "strict", "enabled"].includes(token)) return true; if (["0", "false", "no", "n", "off", "compat", "disabled"].includes(token)) return false; return null; }; const resolveStrictMode = (metadata, strictInputRaw) => { const strictInputParsed = parseBoolToken(strictInputRaw); if (String(strictInputRaw || "").trim() !== "" && strictInputParsed === null) { throw new Error("invalid FINANCE_METADATA_STRICT_MODE (expected bool-like token)"); } if (strictInputParsed !== null) return strictInputParsed; for (const key of ["finance_metadata_strict_mode", "finance_metadata_arrived", "metadata_arrived", "arrival_confirmed", "strict_mode"]) { if (Object.prototype.hasOwnProperty.call(metadata, key)) { const parsed = parseBoolToken(metadata[key]); if (parsed !== null) return parsed; } } for (const key of ["finance_metadata_arrived_at", "metadata_arrived_at", "arrival_marked_at"]) { if (typeof metadata[key] === "string" && metadata[key].trim() !== "") { return true; } } return false; }; const resolveFinanceMetadataContext = (inputPath, strictInputRaw) => { const fallback = { strict_mode: false, anchor_ready: false, anchor: { export_job_url: FINANCE_METADATA_PLACEHOLDER, export_screenshot_ref: FINANCE_METADATA_PLACEHOLDER } }; if (!inputPath) return fallback; let raw; try { raw = fs.readFileSync(inputPath, "utf8"); } catch { return fallback; } let metadata; try { metadata = JSON.parse(raw); } catch { return fallback; } if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { return fallback; } const strictMode = resolveStrictMode(metadata, strictInputRaw); const anchor = { export_job_url: normalizeAnchor(metadata.export_job_url), export_screenshot_ref: normalizeAnchor(metadata.export_screenshot_ref) }; const anchorReady = anchor.export_job_url !== FINANCE_METADATA_PLACEHOLDER && anchor.export_screenshot_ref !== FINANCE_METADATA_PLACEHOLDER; return { strict_mode: strictMode, anchor_ready: anchorReady, anchor }; }; const financeMetadataContext = resolveFinanceMetadataContext( metadataPath, process.env.FINANCE_METADATA_STRICT_MODE || "" ); const payload = { meta_version: 1, artifact_type: "close-loop-meta", generated_at_utc: new Date().toISOString(), run_id: runId, run_attempt: process.env.GITHUB_RUN_ATTEMPT || "", commit_sha: process.env.GITHUB_SHA || "", workflow_ref: process.env.GITHUB_WORKFLOW_REF || "", finance_metadata_strict_mode: financeMetadataContext.strict_mode, finance_metadata_anchor_ready: financeMetadataContext.anchor_ready, finance_metadata_anchor: financeMetadataContext.anchor, links: { result_json_path: resultJsonPath, debug_logs_dir: debugDir } }; fs.mkdirSync(path.dirname(metaPath), { recursive: true }); fs.writeFileSync(metaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); NODE then printf '%s\n' "NO_FILES run_id=${{ github.run_id }} run_attempt=${{ github.run_attempt }} step=generate_meta" > "$CI_ARTIFACT_DIR/close-loop/meta/NO_FILES.txt" fi if ! ls "$CI_ARTIFACT_DIR/close-loop/meta/"*.json >/dev/null 2>&1; then printf '%s\n' "NO_FILES run_id=${{ github.run_id }} run_attempt=${{ github.run_attempt }} step=meta_empty" > "$CI_ARTIFACT_DIR/close-loop/meta/NO_FILES.txt" fi - name: Validate close-loop meta contract if: always() run: | set -euo pipefail META_DIR="$CI_ARTIFACT_DIR/close-loop/meta" shopt -s nullglob meta_files=("$META_DIR"/*.json) if [[ ${#meta_files[@]} -eq 0 ]]; then echo "::error::meta contract failed: no json files under $META_DIR" exit 1 fi for meta_file in "${meta_files[@]}"; do node -e ' const fs = require("fs"); const metaFile = process.argv[1]; const requiredFields = ["run_id", "run_attempt", "commit_sha", "workflow_ref"]; const expectedRunId = process.env.CLOSE_LOOP_RUN_ID || ""; const expectedRunAttempt = process.env.GITHUB_RUN_ATTEMPT || ""; const expectedCommitSha = process.env.GITHUB_SHA || ""; const expectedWorkflowRef = process.env.GITHUB_WORKFLOW_REF || ""; const financeMetadataPlaceholder = process.env.FINANCE_METADATA_PLACEHOLDER || "PENDING_FINANCE_METADATA"; let payload; try { payload = JSON.parse(fs.readFileSync(metaFile, "utf8")); } catch (error) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=invalid_json message=" + error.message); process.exit(1); } if (!payload || typeof payload !== "object" || Array.isArray(payload)) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=invalid_structure message=root_must_be_object"); process.exit(1); } for (const field of requiredFields) { if (!(field in payload)) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=missing_field field=" + field); process.exit(1); } } if (payload.meta_version !== 1) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=invalid_meta_version expected=1 actual=" + payload.meta_version); process.exit(1); } for (const field of requiredFields) { const value = payload[field]; if (typeof value !== "string" || value.trim() === "") { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=empty_or_invalid field=" + field); process.exit(1); } } if (payload.run_id !== expectedRunId) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=context_mismatch field=run_id expected=" + expectedRunId + " actual=" + payload.run_id); process.exit(1); } if (payload.run_attempt !== expectedRunAttempt) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=context_mismatch field=run_attempt expected=" + expectedRunAttempt + " actual=" + payload.run_attempt); process.exit(1); } if (payload.commit_sha !== expectedCommitSha) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=context_mismatch field=commit_sha expected=" + expectedCommitSha + " actual=" + payload.commit_sha); process.exit(1); } if (payload.workflow_ref !== expectedWorkflowRef) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=context_mismatch field=workflow_ref expected=" + expectedWorkflowRef + " actual=" + payload.workflow_ref); process.exit(1); } if (!payload.links || typeof payload.links !== "object" || Array.isArray(payload.links)) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=invalid_links_object"); process.exit(1); } if (typeof payload.links.result_json_path !== "string" || payload.links.result_json_path.trim() === "") { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=empty_or_invalid field=links.result_json_path"); process.exit(1); } if (typeof payload.links.debug_logs_dir !== "string" || payload.links.debug_logs_dir.trim() === "") { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=empty_or_invalid field=links.debug_logs_dir"); process.exit(1); } if (typeof payload.finance_metadata_strict_mode !== "boolean") { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=invalid_type field=finance_metadata_strict_mode expected=boolean"); process.exit(1); } if (typeof payload.finance_metadata_anchor_ready !== "boolean") { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=invalid_type field=finance_metadata_anchor_ready expected=boolean"); process.exit(1); } if (!payload.finance_metadata_anchor || typeof payload.finance_metadata_anchor !== "object" || Array.isArray(payload.finance_metadata_anchor)) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=invalid_finance_metadata_anchor_object"); process.exit(1); } let computedReady = true; for (const field of ["export_job_url", "export_screenshot_ref"]) { if (!(field in payload.finance_metadata_anchor)) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=missing_field field=finance_metadata_anchor." + field); process.exit(1); } if (typeof payload.finance_metadata_anchor[field] !== "string") { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=invalid_type field=finance_metadata_anchor." + field + " expected=string"); process.exit(1); } const anchorValue = payload.finance_metadata_anchor[field].trim(); if (anchorValue === "" || anchorValue === financeMetadataPlaceholder) { computedReady = false; } if (payload.finance_metadata_strict_mode) { if (anchorValue === "" || anchorValue === financeMetadataPlaceholder) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=strict_mode_placeholder field=finance_metadata_anchor." + field); process.exit(1); } } } if (payload.finance_metadata_anchor_ready !== computedReady) { console.error("META_CONTRACT_FAIL file=" + metaFile + " reason=anchor_ready_mismatch expected=" + computedReady + " actual=" + payload.finance_metadata_anchor_ready); process.exit(1); } console.log("META_CONTRACT_OK file=" + metaFile); ' "$meta_file" done - name: Validate close-loop result-json contract if: always() run: | set -euo pipefail RESULT_JSON_DIR="$CI_ARTIFACT_DIR/close-loop/result-json" shopt -s nullglob result_files=("$RESULT_JSON_DIR"/*.json) if [[ ${#result_files[@]} -eq 0 ]]; then echo "::error::result-json contract failed: no json files under $RESULT_JSON_DIR" exit 1 fi for result_file in "${result_files[@]}"; do node -e ' const fs = require("fs"); const resultFile = process.argv[1]; const requiredFields = ["run_id", "status", "stage", "exit_code", "mapped_from"]; const financeMetadataPlaceholder = process.env.FINANCE_METADATA_PLACEHOLDER || "PENDING_FINANCE_METADATA"; let payload; try { payload = JSON.parse(fs.readFileSync(resultFile, "utf8")); } catch (error) { console.error("CONTRACT_FAIL file=" + resultFile + " reason=invalid_json message=" + error.message); process.exit(1); } if (!payload || typeof payload !== "object" || Array.isArray(payload)) { console.error("CONTRACT_FAIL file=" + resultFile + " reason=invalid_structure message=root_must_be_object"); process.exit(1); } for (const field of requiredFields) { if (!(field in payload)) { console.error("CONTRACT_FAIL file=" + resultFile + " reason=missing_field field=" + field); process.exit(1); } } for (const field of ["run_id", "status", "stage", "mapped_from"]) { const value = payload[field]; if (typeof value !== "string" || value.trim() === "") { console.error("CONTRACT_FAIL file=" + resultFile + " reason=empty_or_invalid field=" + field); process.exit(1); } } if (!Number.isFinite(payload.exit_code)) { console.error("CONTRACT_FAIL file=" + resultFile + " reason=empty_or_invalid field=exit_code"); process.exit(1); } if (typeof payload.finance_metadata_strict_mode !== "boolean") { console.error("CONTRACT_FAIL file=" + resultFile + " reason=invalid_type field=finance_metadata_strict_mode expected=boolean"); process.exit(1); } if (typeof payload.finance_metadata_anchor_ready !== "boolean") { console.error("CONTRACT_FAIL file=" + resultFile + " reason=invalid_type field=finance_metadata_anchor_ready expected=boolean"); process.exit(1); } if (!payload.finance_metadata_anchor || typeof payload.finance_metadata_anchor !== "object" || Array.isArray(payload.finance_metadata_anchor)) { console.error("CONTRACT_FAIL file=" + resultFile + " reason=invalid_finance_metadata_anchor_object"); process.exit(1); } let computedReady = true; for (const field of ["export_job_url", "export_screenshot_ref"]) { if (!(field in payload.finance_metadata_anchor)) { console.error("CONTRACT_FAIL file=" + resultFile + " reason=missing_field field=finance_metadata_anchor." + field); process.exit(1); } if (typeof payload.finance_metadata_anchor[field] !== "string") { console.error("CONTRACT_FAIL file=" + resultFile + " reason=invalid_type field=finance_metadata_anchor." + field + " expected=string"); process.exit(1); } const anchorValue = payload.finance_metadata_anchor[field].trim(); if (anchorValue === "" || anchorValue === financeMetadataPlaceholder) { computedReady = false; } if (payload.finance_metadata_strict_mode) { if (anchorValue === "" || anchorValue === financeMetadataPlaceholder) { console.error("CONTRACT_FAIL file=" + resultFile + " reason=strict_mode_placeholder field=finance_metadata_anchor." + field); process.exit(1); } } } if (payload.finance_metadata_anchor_ready !== computedReady) { console.error("CONTRACT_FAIL file=" + resultFile + " reason=anchor_ready_mismatch expected=" + computedReady + " actual=" + payload.finance_metadata_anchor_ready); process.exit(1); } console.log("CONTRACT_OK file=" + resultFile); ' "$result_file" done - name: Validate finance metadata anchor consistency if: always() run: | set -euo pipefail META_DIR="$CI_ARTIFACT_DIR/close-loop/meta" RESULT_JSON_DIR="$CI_ARTIFACT_DIR/close-loop/result-json" node - "$META_DIR" "$RESULT_JSON_DIR" <<'NODE' const fs = require("fs"); const path = require("path"); const [metaDir, resultDir] = process.argv.slice(2); const anchorFields = ["export_job_url", "export_screenshot_ref"]; const readJsonFiles = (dir, label) => { const names = fs.readdirSync(dir, { withFileTypes: true }) .filter((entry) => entry.isFile() && entry.name.endsWith(".json")) .map((entry) => path.join(dir, entry.name)) .sort(); if (names.length === 0) { throw new Error(`${label}_NO_JSON_FILES dir=${dir}`); } return names; }; const loadPayloadsByRunId = (files, label) => { const map = new Map(); for (const file of files) { let payload; try { payload = JSON.parse(fs.readFileSync(file, "utf8")); } catch (error) { throw new Error(`${label}_INVALID_JSON file=${file} message=${error.message}`); } if (!payload || typeof payload !== "object" || Array.isArray(payload)) { throw new Error(`${label}_INVALID_STRUCTURE file=${file}`); } const runId = typeof payload.run_id === "string" ? payload.run_id.trim() : ""; if (!runId) { throw new Error(`${label}_EMPTY_RUN_ID file=${file}`); } if (map.has(runId)) { throw new Error(`${label}_DUPLICATE_RUN_ID run_id=${runId}`); } if (typeof payload.finance_metadata_strict_mode !== "boolean") { throw new Error(`${label}_INVALID_STRICT_MODE file=${file}`); } if (typeof payload.finance_metadata_anchor_ready !== "boolean") { throw new Error(`${label}_INVALID_ANCHOR_READY file=${file}`); } if (!payload.finance_metadata_anchor || typeof payload.finance_metadata_anchor !== "object" || Array.isArray(payload.finance_metadata_anchor)) { throw new Error(`${label}_INVALID_ANCHOR_OBJECT file=${file}`); } for (const field of anchorFields) { if (typeof payload.finance_metadata_anchor[field] !== "string") { throw new Error(`${label}_INVALID_ANCHOR_FIELD file=${file} field=finance_metadata_anchor.${field}`); } } map.set(runId, { file, payload }); } return map; }; const metaMap = loadPayloadsByRunId(readJsonFiles(metaDir, "META"), "META"); const resultMap = loadPayloadsByRunId(readJsonFiles(resultDir, "RESULT"), "RESULT"); for (const [runId, metaEntry] of metaMap.entries()) { const resultEntry = resultMap.get(runId); if (!resultEntry) { throw new Error(`ANCHOR_CONSISTENCY_FAIL reason=missing_result_json run_id=${runId} meta_file=${metaEntry.file}`); } for (const field of anchorFields) { const metaValue = metaEntry.payload.finance_metadata_anchor[field]; const resultValue = resultEntry.payload.finance_metadata_anchor[field]; if (metaValue !== resultValue) { throw new Error( `ANCHOR_CONSISTENCY_FAIL reason=field_mismatch run_id=${runId} field=finance_metadata_anchor.${field} ` + `meta_value=${JSON.stringify(metaValue)} result_value=${JSON.stringify(resultValue)} ` + `meta_file=${metaEntry.file} result_file=${resultEntry.file}` ); } } if (metaEntry.payload.finance_metadata_strict_mode !== resultEntry.payload.finance_metadata_strict_mode) { throw new Error( `ANCHOR_CONSISTENCY_FAIL reason=strict_mode_mismatch run_id=${runId} ` + `meta_value=${metaEntry.payload.finance_metadata_strict_mode} result_value=${resultEntry.payload.finance_metadata_strict_mode} ` + `meta_file=${metaEntry.file} result_file=${resultEntry.file}` ); } if (metaEntry.payload.finance_metadata_anchor_ready !== resultEntry.payload.finance_metadata_anchor_ready) { throw new Error( `ANCHOR_CONSISTENCY_FAIL reason=anchor_ready_mismatch run_id=${runId} ` + `meta_value=${metaEntry.payload.finance_metadata_anchor_ready} result_value=${resultEntry.payload.finance_metadata_anchor_ready} ` + `meta_file=${metaEntry.file} result_file=${resultEntry.file}` ); } } for (const [runId, resultEntry] of resultMap.entries()) { if (!metaMap.has(runId)) { throw new Error(`ANCHOR_CONSISTENCY_FAIL reason=missing_meta run_id=${runId} result_file=${resultEntry.file}`); } } console.log(`ANCHOR_CONSISTENCY_OK run_ids=${metaMap.size}`); NODE - name: Upload finance gate meta artifact if: always() uses: actions/upload-artifact@v4 with: name: cycle043-finance-gate-meta-${{ github.run_id }}-${{ github.run_attempt }} path: projects/ar-collections-assistant/.ci-artifacts/cycle-043/close-loop/meta/** if-no-files-found: warn retention-days: 90 - name: Upload finance gate result-json artifact if: always() uses: actions/upload-artifact@v4 with: name: cycle043-finance-gate-result-json-${{ github.run_id }}-${{ github.run_attempt }} path: projects/ar-collections-assistant/.ci-artifacts/cycle-043/close-loop/result-json/** if-no-files-found: warn retention-days: 90 - name: Upload finance gate debug-logs artifact if: always() uses: actions/upload-artifact@v4 with: name: cycle043-finance-gate-debug-logs-${{ github.run_id }}-${{ github.run_attempt }} path: projects/ar-collections-assistant/.ci-artifacts/cycle-043/debug/** if-no-files-found: warn retention-days: 14