diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84353db..858741f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: pull_request: permissions: + actions: read contents: read concurrency: @@ -56,12 +57,26 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Fetch public evidence comparison base + env: + PUBLIC_EVIDENCE_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + run: | + if [ -n "$PUBLIC_EVIDENCE_BASE_SHA" ] && [ "$PUBLIC_EVIDENCE_BASE_SHA" != "0000000000000000000000000000000000000000" ]; then + git fetch --no-tags --depth=1 origin "$PUBLIC_EVIDENCE_BASE_SHA" + fi + - name: Install evidence replay tools run: | sudo apt-get update - sudo apt-get install --yes --no-install-recommends ffmpeg + sudo apt-get install --yes --no-install-recommends ffmpeg zip unzip ffmpeg -version | head -n 1 + - name: Verify public evidence append-only evolution + env: + GH_TOKEN: ${{ github.token }} + PUBLIC_EVIDENCE_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + run: pnpm verify:public-economic-resilience-evolution -- --base "${PUBLIC_EVIDENCE_BASE_SHA:-0000000000000000000000000000000000000000}" + - name: Verify package run: pnpm verify diff --git a/README.md b/README.md index 8cf7e30..91301de 100644 --- a/README.md +++ b/README.md @@ -373,8 +373,9 @@ Important limitations: ## Development verification -The full repository gate requires `ffmpeg` and `ffprobe` so retained and -newly generated native image evidence can be decoded and visually replayed. +The full repository gate requires `ffmpeg`, `ffprobe`, `zipinfo`, and `unzip` +so retained and newly generated native image evidence can be decoded, boundedly +inspected, and visually replayed. The pinned Docker lane includes both tools. ```bash @@ -394,6 +395,8 @@ pnpm example:ios:transformer-test pnpm docs:check pnpm site:check pnpm site:build +pnpm verify:public-economic-resilience-evidence +pnpm verify:public-economic-resilience-evolution -- --base pnpm fixtures:compatibility:check git diff --check pnpm pack --dry-run @@ -429,6 +432,21 @@ conversion and allows only a 0.001 SSIM implementation tolerance after both measurements independently pass the quality and orientation gates. The bundle is an environment-specific observation, not a speed ranking, cost-savings claim, or real-device benchmark. +Exact-master Android and iOS bundles can be retained separately from the +published-package demo in an append-only, full-SHA-addressed public archive. +`pnpm import:public-economic-resilience-evidence -- --run-id ` queries the +GitHub run and artifact APIs directly, requires one successful +`workflow_dispatch` run on `refs/heads/master`, downloads the exact Android and +iOS artifacts by ID, and retains their digest-bound ZIP bytes. It refuses +replacement or orphaned index state and binds byte-identical source/fixture +assets across platforms. +`pnpm verify:public-economic-resilience-evidence` independently replays the +portable visual contract and ZIP-to-file equality for every retained bundle. +The CI evolution gate prevents deletion, reordering, or byte changes to any +earlier capture. The empty archive state is valid; the +[economic resilience page](https://ggulbae.github.io/react-native-image-compression-kit/reference/economic-resilience) +shows a methodology preview while empty and switches to exact archive-derived +metrics, assets, and workflow provenance after a capture exists. Comparison dependencies remain inside the private example application and outside the published package. See the [benchmark methodology](docs/benchmarks/README.md) for its timing boundary, diff --git a/docs/verification-architecture.md b/docs/verification-architecture.md index dba8038..57d21a1 100644 --- a/docs/verification-architecture.md +++ b/docs/verification-architecture.md @@ -17,6 +17,7 @@ from the npm package. | Native demo result metrics, source/output/screenshot/recording bytes, auto-oriented SSIM and vertical-flip control, ordered walkthrough timing, parsed video-track duration, timestamp normalization, digests, platform provenance, and exact source identity | `test/demoEvidence.test.mjs`, `test/demoVisualAgreement.test.mjs`, `test/guidedDemoCore.test.mjs`, `test/demoCaptureScriptContract.test.ts`, `scripts/demo-evidence-core.mjs`, `scripts/demo-visual-agreement-core.mjs`, and `scripts/guided-demo-core.mjs` | Native Demo Evidence workflow and `pnpm verify:demo-evidence` | | Native baseline and exact-plan implementation comparison metrics, balanced sample positions, comparator identity, fixture/plan bytes, digests, and platform provenance | `test/benchmark.test.mjs`, `test/benchmarkComparison.test.mjs`, `scripts/benchmark-core.mjs`, and `scripts/benchmark-comparison-core.mjs` | Native Demo Evidence workflow, `pnpm verify:benchmark-evidence`, and `pnpm verify:benchmark-comparison-evidence` | | Kit-only 12 MP source/output bytes, exact environment and capabilities, call-only timing, decode/geometry, a `strip` request with no APP1/APP13/JPEG comments, explicit full-to-limited-range SSIM/flip control with a 0.001 replay tolerance, signed byte difference, and zero package-output residuals | `test/economicResilienceBenchmark.test.mjs`, `test/economicResilienceEvidence.test.mjs`, `test/economicResilienceNativeSourceContract.test.mjs`, and `scripts/economic-resilience-evidence-core.mjs` | Native Demo Evidence workflow and `pnpm verify:economic-resilience-evidence` | +| Append-only exact-master 12 MP archive topology, GitHub-bound run/artifact identity, retained ZIP-to-file equality, cross-platform source identity, economic claim boundary, crash recovery, revision evolution with live reacquisition for new suffixes, and independent portable visual replay | `test/publicEconomicResilienceEvidence.test.mjs`, `test/publicEconomicResilienceEvolution.test.mjs`, `scripts/public-economic-resilience-evidence-core.mjs`, `scripts/public-economic-resilience-github.mjs`, and `scripts/public-economic-resilience-evolution-core.mjs` | `pnpm import:public-economic-resilience-evidence -- --run-id `, `pnpm verify:public-economic-resilience-evidence`, and `pnpm verify:public-economic-resilience-evolution -- --base ` | | Packed-consumer compatibility lane definitions | `test/compatibilityMatrix.test.mjs` and `scripts/compatibility-matrix-core.mjs` | `pnpm fixtures:compatibility:check` and the Compatibility workflow | | Built public-site performance, accessibility, and SEO | `scripts/verify-site-quality.mjs` | `pnpm site:build && pnpm site:quality` | | Repository metadata, security features, Actions policy, rulesets, environments, and Pages | `test/repositorySettings.test.mjs`, `docs/repository-settings.json`, and `scripts/repository-settings-core.mjs` | `pnpm fixtures:repository-settings:check` and `pnpm audit:repository-settings` | @@ -72,6 +73,8 @@ pnpm docs:check pnpm site:check pnpm site:build pnpm site:quality +pnpm verify:public-economic-resilience-evidence +pnpm verify:public-economic-resilience-evolution -- --base pnpm fixtures:compatibility:check pnpm fixtures:repository-settings:check pnpm audit:repository-settings @@ -101,6 +104,8 @@ smoke test in their supported environments. methodology documentation together. - Change workflow actions or pins in the workflow supply-chain verifier. - Change release evidence rules in the evidence-specific verifier and fixtures. +- Change public source-tree evidence in the append-only importer, archive + verifier, methodology page, and archive mutation tests together. ## Non-goals diff --git a/package.json b/package.json index d28edc0..ddc3600 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,9 @@ "verify:benchmark-comparison-evidence": "node scripts/verify-benchmark-comparison-evidence.mjs", "economic-resilience:evidence": "node scripts/create-economic-resilience-evidence.mjs", "verify:economic-resilience-evidence": "node scripts/verify-economic-resilience-evidence.mjs", + "import:public-economic-resilience-evidence": "node scripts/import-public-economic-resilience-evidence.mjs", + "verify:public-economic-resilience-evidence": "node scripts/verify-public-economic-resilience-evidence.mjs", + "verify:public-economic-resilience-evolution": "node scripts/verify-public-economic-resilience-evolution.mjs", "merge:demo-evidence": "node scripts/merge-demo-evidence.mjs", "release:dry-run": "node scripts/release-dry-run.mjs", "smoke:consumer": "pnpm build && node scripts/consumer-smoke-test.mjs", @@ -116,7 +119,7 @@ "verify:action-pin-attestation": "node scripts/verify-action-pin-attestation.mjs", "verify:action-pin-fixture": "pnpm verify:action-pin-provenance -- --artifact-dir test/fixtures/action-pin-review --json", "verify:action-pin-attestation-fixture": "pnpm verify:action-pin-attestation -- --artifact-dir test/fixtures/action-pin-review --attestation-bundle test/fixtures/action-pin-attestation/attestation.jsonl --trusted-root test/fixtures/action-pin-attestation/trusted-root.jsonl --json", - "verify": "pnpm typecheck && pnpm test:coverage && pnpm build && pnpm docs:check && pnpm site:check && pnpm fixtures:compatibility:check && pnpm fixtures:ios-pass-replay:audit && pnpm fixtures:release-evidence-acquisition:check && pnpm fixtures:release-evidence-review-acquisition:check && pnpm verify:release-evidence-set -- --json && pnpm verify:release-evidence-review-archive-set -- --json && pnpm verify:dependency-security -- --json && pnpm verify:workflow-supply-chain -- --json && pnpm verify:action-pin-fixture && pnpm verify:action-pin-attestation-fixture && pnpm android:doctor", + "verify": "pnpm typecheck && pnpm test:coverage && pnpm build && pnpm docs:check && pnpm site:check && pnpm verify:public-economic-resilience-evidence && pnpm fixtures:compatibility:check && pnpm fixtures:ios-pass-replay:audit && pnpm fixtures:release-evidence-acquisition:check && pnpm fixtures:release-evidence-review-acquisition:check && pnpm verify:release-evidence-set -- --json && pnpm verify:release-evidence-review-archive-set -- --json && pnpm verify:dependency-security -- --json && pnpm verify:workflow-supply-chain -- --json && pnpm verify:action-pin-fixture && pnpm verify:action-pin-attestation-fixture && pnpm android:doctor", "android:doctor": "node scripts/android-verification.mjs doctor", "android:codegen": "node scripts/android-verification.mjs codegen", "android:build": "node scripts/android-verification.mjs build", diff --git a/scripts/docs-semantic-core.mjs b/scripts/docs-semantic-core.mjs index 746769e..199d685 100644 --- a/scripts/docs-semantic-core.mjs +++ b/scripts/docs-semantic-core.mjs @@ -5,6 +5,10 @@ import { statSync, } from 'node:fs'; import path from 'node:path'; +import { + PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT, + inspectPublicEconomicResilienceArchive, +} from './public-economic-resilience-evidence-core.mjs'; export const STATUS_START = ''; export const STATUS_END = ''; @@ -48,6 +52,7 @@ export const REQUIRED_DOCUMENTATION_FILES = [ 'docs/product-architecture.md', 'docs/verification-architecture.md', 'website/reference/evidence.md', + 'website/reference/economic-resilience.md', 'docs/maintainers/account-recovery.md', 'docs/maintainers/repository-settings.md', 'docs/maintainers/trusted-release.md', @@ -629,6 +634,7 @@ export function inspectDocumentation(root) { 'pnpm example:typecheck', 'pnpm docs:check', 'pnpm site:check', + 'pnpm verify:public-economic-resilience-evidence', 'pnpm fixtures:compatibility:check', 'pnpm fixtures:repository-settings:check', 'pnpm audit:repository-settings', @@ -662,6 +668,37 @@ export function inspectDocumentation(root) { ); } + const economicEvidencePath = path.join( + root, + 'website/reference/economic-resilience.md' + ); + if (existsSync(economicEvidencePath)) { + errors.push( + ...inspectEconomicResiliencePageContracts( + readFileSync(economicEvidencePath, 'utf8') + ) + ); + const publicArchive = inspectPublicEconomicResilienceArchive( + path.join(root, PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT) + ); + if (publicArchive.status !== 'passed') { + errors.push(`public economic resilience archive: ${publicArchive.error}`); + } else { + const componentPath = path.join( + root, + 'website/.vitepress/theme/EconomicResilienceArchive.vue' + ); + errors.push( + ...inspectEconomicResilienceStateContracts({ + archiveState: publicArchive.archiveState, + componentContents: existsSync(componentPath) + ? readFileSync(componentPath, 'utf8') + : '', + }) + ); + } + } + inspectPublicLaunchContracts( root, packageJson.version, @@ -898,6 +935,61 @@ export function inspectProductEvidenceContracts(contents) { return errors; } +export function inspectEconomicResiliencePageContracts(contents) { + const errors = []; + inspectDecisionDocument({ + label: 'economic resilience evidence', + contents, + headings: [ + 'What a capture must prove', + 'Economic claim boundary', + 'Append-only archive layout', + 'Import and verify', + ], + snippets: [ + '', + 'source-remains', + 'matchedTransferBaseline: null', + 'costSavingsClaim: null', + 'workflow_dispatch', + 'refs/heads/master', + 'pnpm verify:public-economic-resilience-evidence', + 'There is no mutable `latest`', + ], + errors, + }); + return errors; +} + +export function inspectEconomicResilienceStateContracts({ + archiveState, + componentContents, +}) { + const errors = []; + const required = archiveState === 'available' + ? [ + 'Archived source-tree capture', + 'sourceToOutputByteDifference', + 'uprightSimilarity', + 'verticalFlipSimilarity', + 'removedPackageOutputs', + 'state.runUrl', + 'Original artifact ZIP', + ] + : [ + 'Methodology preview · no archived capture', + 'not a package release or a measured product result', + ]; + for (const snippet of required) { + if (!componentContents.includes(snippet)) { + errors.push( + `economic resilience ${archiveState} state missing component contract: ${snippet}` + ); + } + } + return errors; +} + function inspectDecisionDocument({ label, contents, diff --git a/scripts/economic-resilience-replay.mjs b/scripts/economic-resilience-replay.mjs new file mode 100644 index 0000000..d103d7e --- /dev/null +++ b/scripts/economic-resilience-replay.mjs @@ -0,0 +1,46 @@ +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const SCRIPT_DIRECTORY = path.dirname(fileURLToPath(import.meta.url)); + +export function replayEconomicResilienceArtifact(artifactDir) { + const result = spawnSync( + process.execPath, + [ + path.join(SCRIPT_DIRECTORY, 'verify-economic-resilience-evidence.mjs'), + '--artifact-dir', + path.resolve(artifactDir), + ], + { + cwd: path.resolve(SCRIPT_DIRECTORY, '..'), + encoding: 'utf8', + maxBuffer: 32 * 1024 * 1024, + } + ); + if (result.error) throw result.error; + let report; + try { + report = JSON.parse(result.stdout); + } catch (error) { + throw new Error( + `economic resilience verifier returned invalid JSON: ${error.message}` + ); + } + if ( + result.status !== 0 || + report?.status !== 'passed' || + report?.replay?.status !== 'passed' || + report?.replay?.measurementMatch !== true || + report?.replay?.outcomesPassed !== true || + report?.replay?.exactShapes !== true || + report?.replay?.stableFieldsMatch !== true + ) { + throw new Error( + report?.error || + result.stderr.trim() || + 'economic resilience independent visual replay failed' + ); + } + return report; +} diff --git a/scripts/import-public-economic-resilience-evidence.mjs b/scripts/import-public-economic-resilience-evidence.mjs new file mode 100644 index 0000000..1ecc205 --- /dev/null +++ b/scripts/import-public-economic-resilience-evidence.mjs @@ -0,0 +1,655 @@ +#!/usr/bin/env node + +import { createHash, randomUUID } from 'node:crypto'; +import { + constants, + copyFileSync, + existsSync, + lstatSync, + mkdirSync, + readFileSync, + readdirSync, + realpathSync, + renameSync, + rmSync, + rmdirSync, + unlinkSync, + writeFileSync, +} from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { ECONOMIC_RESILIENCE_ASSET_FILES } from './economic-resilience-evidence-core.mjs'; +import { replayEconomicResilienceArtifact } from './economic-resilience-replay.mjs'; +import { + PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT, + PUBLIC_ECONOMIC_RESILIENCE_ARTIFACT_METADATA_FILE, + PUBLIC_ECONOMIC_RESILIENCE_CAPTURE_SET_FILE, + PUBLIC_ECONOMIC_RESILIENCE_INDEX_FILE, + PUBLIC_ECONOMIC_RESILIENCE_RUN_METADATA_FILE, + appendPublicEconomicResilienceIndex, + buildPublicEconomicResilienceCaptureSet, + createEmptyPublicEconomicResilienceIndex, + inspectPublicEconomicResilienceArchive, +} from './public-economic-resilience-evidence-core.mjs'; +import { acquirePublicEconomicResilienceRun } from './public-economic-resilience-github.mjs'; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const options = parseArgs(process.argv.slice(2)); +if (!options.runId) throw new Error('--run-id is required'); +const runId = Number(options.runId); +if (!Number.isSafeInteger(runId) || runId <= 0 || String(runId) !== options.runId) { + throw new Error('--run-id must be a positive safe integer'); +} +const archiveRoot = path.resolve( + options.archiveRoot ?? + path.join(repositoryRoot, PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT) +); +const filesystemRoot = path.parse(archiveRoot).root; +if ( + archiveRoot === filesystemRoot || + archiveRoot === repositoryRoot || + path.dirname(archiveRoot) === filesystemRoot +) { + throw new Error('--archive-root must identify a scoped archive below a non-root parent'); +} + +let acquisition = null; +let transaction = null; +let completed = false; +const interrupt = () => { + try { + if (transaction) recoverOrCleanTransaction(transaction); + if (transaction) releaseLock(transaction); + if (acquisition) acquisition.cleanup(); + } finally { + process.exit(130); + } +}; +process.once('SIGINT', interrupt); +process.once('SIGTERM', interrupt); + +try { + acquisition = acquirePublicEconomicResilienceRun(runId); + const androidArtifact = acquisition.platforms.android.artifactRoot; + const iosArtifact = acquisition.platforms.ios.artifactRoot; + const androidEvidence = readEvidence(androidArtifact, 'Android'); + const iosEvidence = readEvidence(iosArtifact, 'iOS'); + const captureSet = buildPublicEconomicResilienceCaptureSet({ + sourceCommit: acquisition.sourceCommit, + runMetadata: acquisition.runMetadata, + runMetadataBytes: acquisition.runMetadataBytes, + artifactMetadata: acquisition.artifactMetadata, + artifactMetadataBytes: acquisition.artifactMetadataBytes, + androidEvidence, + iosEvidence, + }); + assertSameBytes( + path.join(androidArtifact, 'source.jpg'), + path.join(iosArtifact, 'source.jpg'), + 'Android and iOS source assets differ' + ); + assertSameBytes( + path.join(androidArtifact, 'fixture-manifest.json'), + path.join(iosArtifact, 'fixture-manifest.json'), + 'Android and iOS fixture manifests differ' + ); + const inputReplay = { + android: replayEconomicResilienceArtifact(androidArtifact), + ios: replayEconomicResilienceArtifact(iosArtifact), + }; + + transaction = beginTransaction({ archiveRoot }); + const currentReport = inspectPublicEconomicResilienceArchive( + transaction.archiveRoot + ); + if (currentReport.status !== 'passed') throw new Error(currentReport.error); + for (const capture of currentReport.captures) { + for (const platform of ['android', 'ios']) { + replayEconomicResilienceArtifact( + path.join( + transaction.archiveRoot, + capture.platforms[platform].artifactPath + ) + ); + } + } + + const indexPath = path.join( + transaction.archiveRoot, + PUBLIC_ECONOMIC_RESILIENCE_INDEX_FILE + ); + const baseIndexBytes = existsSync(indexPath) + ? readSecureSingleLinkFile(indexPath, 'public archive index') + : null; + const currentIndex = baseIndexBytes + ? JSON.parse(baseIndexBytes.toString('utf8')) + : createEmptyPublicEconomicResilienceIndex(); + const nextIndex = appendPublicEconomicResilienceIndex(currentIndex, captureSet); + const nextIndexBytes = serializeJson(nextIndex); + + const stageRoot = transaction.stageRoot; + mkdirSync(stageRoot); + const stagedSourceTree = path.join(stageRoot, 'source-tree'); + mkdirSync(stagedSourceTree); + const stagedCapture = path.join(stagedSourceTree, acquisition.sourceCommit); + mkdirSync(stagedCapture); + const stagedArtifacts = path.join(stagedCapture, 'artifacts'); + mkdirSync(stagedArtifacts); + for (const platform of ['android', 'ios']) { + const acquiredPlatform = acquisition.platforms[platform]; + const stagedPlatform = path.join(stagedCapture, platform); + mkdirSync(stagedPlatform); + for (const asset of ECONOMIC_RESILIENCE_ASSET_FILES) { + copyExclusiveSingleLink( + path.join(acquiredPlatform.artifactRoot, asset), + path.join(stagedPlatform, asset), + `${platform} ${asset}` + ); + } + copyExclusiveSingleLink( + acquiredPlatform.zipFile, + path.join(stagedArtifacts, `${platform}.zip`), + `${platform} retained artifact ZIP` + ); + } + writeExclusive( + path.join(stagedCapture, PUBLIC_ECONOMIC_RESILIENCE_CAPTURE_SET_FILE), + serializeJson(captureSet) + ); + writeExclusive( + path.join(stagedCapture, PUBLIC_ECONOMIC_RESILIENCE_RUN_METADATA_FILE), + acquisition.runMetadataBytes + ); + writeExclusive( + path.join(stagedCapture, PUBLIC_ECONOMIC_RESILIENCE_ARTIFACT_METADATA_FILE), + acquisition.artifactMetadataBytes + ); + const stagedIndex = appendPublicEconomicResilienceIndex( + createEmptyPublicEconomicResilienceIndex(), + captureSet + ); + writeExclusive(path.join(stageRoot, PUBLIC_ECONOMIC_RESILIENCE_INDEX_FILE), + serializeJson(stagedIndex)); + const stagedReport = inspectPublicEconomicResilienceArchive(stageRoot); + if (stagedReport.status !== 'passed') { + throw new Error(`staged public archive failed verification: ${stagedReport.error}`); + } + replayEconomicResilienceArtifact(path.join(stagedCapture, 'android')); + replayEconomicResilienceArtifact(path.join(stagedCapture, 'ios')); + + const nextIndexPath = transaction.nextIndexPath; + writeExclusive(nextIndexPath, nextIndexBytes); + const destinationCapture = path.join( + transaction.archiveRoot, + 'source-tree', + acquisition.sourceCommit + ); + const journal = { + schemaVersion: 1, + pid: process.pid, + sourceCommit: acquisition.sourceCommit, + archiveRoot: transaction.archiveRoot, + stagedPath: stageRoot, + destinationCapture, + nextIndexPath, + baseIndexDigest: baseIndexBytes ? sha256(baseIndexBytes) : null, + nextIndexDigest: sha256(nextIndexBytes), + }; + triggerFailpoint('before-journal'); + writeExclusive(transaction.journalPath, serializeJson(journal), 0o600); + transaction.journalWritten = true; + triggerFailpoint('after-journal'); + + if (!existsSync(transaction.archiveRoot)) mkdirSync(transaction.archiveRoot); + else assertRegularDirectory(transaction.archiveRoot, 'public archive root'); + const destinationSourceTree = path.join(transaction.archiveRoot, 'source-tree'); + if (!existsSync(destinationSourceTree)) mkdirSync(destinationSourceTree); + else assertRegularDirectory(destinationSourceTree, 'public archive source-tree'); + assertMissing(destinationCapture, 'source SHA destination already exists'); + assertCurrentIndex(indexPath, baseIndexBytes); + renameSync(stagedCapture, destinationCapture); + triggerFailpoint('after-capture-rename'); + assertCurrentIndex(indexPath, baseIndexBytes); + renameSync(nextIndexPath, indexPath); + triggerFailpoint('after-index-publish'); + + const report = inspectPublicEconomicResilienceArchive(transaction.archiveRoot); + if (report.status !== 'passed') { + throw new Error(`imported public archive failed verification: ${report.error}`); + } + cleanupPublishedTransaction(transaction); + completed = true; + process.stdout.write(`${JSON.stringify({ + ...report, + importedSourceCommit: acquisition.sourceCommit, + replay: { + android: inputReplay.android.replay, + ios: inputReplay.ios.replay, + }, + })}\n`); +} catch (error) { + if (transaction) recoverOrCleanTransaction(transaction); + throw error; +} finally { + process.removeListener('SIGINT', interrupt); + process.removeListener('SIGTERM', interrupt); + if (transaction) releaseLock(transaction); + if (acquisition) acquisition.cleanup(); + if (!completed && transaction?.stageRoot && existsSync(transaction.stageRoot)) { + removeValidatedTree(transaction.stageRoot, transaction.archiveParent); + } +} + +function beginTransaction({ archiveRoot }) { + const createdParents = ensureDirectoryPath(path.dirname(archiveRoot)); + const archiveParent = realpathSync(path.dirname(archiveRoot)); + const canonicalArchiveRoot = path.join(archiveParent, path.basename(archiveRoot)); + const lockPath = path.join(archiveParent, '.economic-resilience-import.lock'); + const journalPath = path.join( + archiveParent, + '.economic-resilience-import.journal.json' + ); + const transactionId = randomUUID(); + const stageRoot = path.join( + archiveParent, + `.economic-resilience-stage-${process.pid}-${transactionId}` + ); + const nextIndexPath = path.join( + archiveParent, + `.economic-resilience-index-${process.pid}-${transactionId}.json` + ); + const transaction = { + archiveRoot: canonicalArchiveRoot, + archiveParent, + lockPath, + journalPath, + createdParents, + stageRoot, + nextIndexPath, + lockOwned: false, + journalWritten: false, + }; + recoverStaleTransactionBeforeLock(transaction); + const lockRecord = { + schemaVersion: 1, + pid: process.pid, + startedAt: new Date().toISOString(), + journalPath, + stagedPath: stageRoot, + nextIndexPath, + }; + writeExclusive(lockPath, serializeJson(lockRecord), 0o600); + transaction.lockRecord = lockRecord; + transaction.lockOwned = true; + if (existsSync(journalPath)) recoverJournal(transaction, lockRecord); + return transaction; +} + +function recoverStaleTransactionBeforeLock(transaction) { + if (!existsSync(transaction.lockPath)) { + if (existsSync(transaction.journalPath)) recoverJournal(transaction); + return; + } + let lock; + try { + const lockBytes = readSecureSingleLinkFile(transaction.lockPath, 'import lock'); + if (lockBytes.length === 0 && !existsSync(transaction.journalPath)) { + unlinkSync(transaction.lockPath); + return; + } + lock = JSON.parse(lockBytes.toString('utf8')); + validateLock(lock, transaction); + } catch (error) { + throw new Error(`public evidence import lock is invalid: ${error.message}`); + } + if (processIsLive(lock?.pid)) { + throw new Error(`public economic evidence import is already active (PID ${lock.pid})`); + } + if (existsSync(transaction.journalPath)) recoverJournal(transaction, lock); + else cleanupLockPlannedFiles(lock, transaction); + unlinkSync(transaction.lockPath); +} + +function cleanupLockPlannedFiles(lock, transaction) { + for (const [candidate, kind] of [ + [lock?.stagedPath, 'stage'], + [lock?.nextIndexPath, 'index'], + ]) { + if (!within(transaction.archiveParent, candidate)) { + throw new Error(`stale import lock ${kind} path is invalid`); + } + } + if (existsSync(lock.stagedPath)) { + removeValidatedTree(lock.stagedPath, transaction.archiveParent); + } + if (existsSync(lock.nextIndexPath)) unlinkSync(lock.nextIndexPath); +} + +function recoverOrCleanTransaction(transaction) { + if (existsSync(transaction.journalPath)) { + recoverJournal(transaction, transaction.lockRecord); + transaction.journalWritten = false; + return; + } + if (transaction.nextIndexPath && existsSync(transaction.nextIndexPath)) { + unlinkSync(transaction.nextIndexPath); + } + if (transaction.stageRoot && existsSync(transaction.stageRoot)) { + removeValidatedTree(transaction.stageRoot, transaction.archiveParent); + } +} + +function recoverJournal(transaction, expectedLock = null) { + const bytes = readSecureSingleLinkFile(transaction.journalPath, 'import journal'); + const journal = JSON.parse(bytes.toString('utf8')); + validateJournal(journal, transaction, expectedLock); + const indexPath = path.join( + transaction.archiveRoot, + PUBLIC_ECONOMIC_RESILIENCE_INDEX_FILE + ); + const currentDigest = existsSync(indexPath) + ? sha256(readSecureSingleLinkFile(indexPath, 'public archive index')) + : null; + const captureExists = existsSync(journal.destinationCapture); + if (!captureExists) { + if (currentDigest !== journal.baseIndexDigest) { + throw new Error('stale import journal cannot roll back a changed archive index'); + } + cleanupJournalFiles(journal, transaction); + cleanupEmptyArchive(transaction.archiveRoot); + return; + } + if (currentDigest === journal.baseIndexDigest) { + const nextBytes = readSecureSingleLinkFile(journal.nextIndexPath, 'next archive index'); + if (sha256(nextBytes) !== journal.nextIndexDigest) { + throw new Error('stale import journal next-index digest differs'); + } + renameSync(journal.nextIndexPath, indexPath); + } else if (currentDigest !== journal.nextIndexDigest) { + throw new Error('stale import journal does not match the current archive index'); + } + const report = inspectPublicEconomicResilienceArchive(transaction.archiveRoot); + if (report.status !== 'passed') { + throw new Error(`recovered public archive failed verification: ${report.error}`); + } + cleanupJournalFiles(journal, transaction); +} + +function validateJournal(journal, transaction, expectedLock) { + const expected = [ + 'archiveRoot', 'baseIndexDigest', 'destinationCapture', 'nextIndexDigest', + 'nextIndexPath', 'pid', 'schemaVersion', 'sourceCommit', 'stagedPath', + ].sort(); + if ( + !journal || typeof journal !== 'object' || Array.isArray(journal) || + JSON.stringify(Object.keys(journal).sort()) !== JSON.stringify(expected) || + journal.schemaVersion !== 1 || + !Number.isSafeInteger(journal.pid) || + journal.pid <= 0 || + journal.archiveRoot !== transaction.archiveRoot || + !/^[0-9a-f]{40}$/u.test(journal.sourceCommit ?? '') || + !digestOrNull(journal.baseIndexDigest) || + !/^[0-9a-f]{64}$/u.test(journal.nextIndexDigest ?? '') || + !within(transaction.archiveParent, journal.stagedPath) || + !within(transaction.archiveParent, journal.nextIndexPath) || + !validTransactionPaths(journal) || + (expectedLock !== null && + (journal.pid !== expectedLock.pid || + journal.stagedPath !== expectedLock.stagedPath || + journal.nextIndexPath !== expectedLock.nextIndexPath)) || + journal.destinationCapture !== path.join( + transaction.archiveRoot, 'source-tree', journal.sourceCommit + ) + ) { + throw new Error('public evidence import journal is invalid'); + } +} + +function validateLock(lock, transaction) { + const expected = [ + 'journalPath', 'nextIndexPath', 'pid', 'schemaVersion', 'stagedPath', + 'startedAt', + ].sort(); + if ( + !lock || typeof lock !== 'object' || Array.isArray(lock) || + JSON.stringify(Object.keys(lock).sort()) !== JSON.stringify(expected) || + lock.schemaVersion !== 1 || + !Number.isSafeInteger(lock.pid) || lock.pid <= 0 || + lock.journalPath !== transaction.journalPath || + typeof lock.startedAt !== 'string' || + !within(transaction.archiveParent, lock.stagedPath) || + !within(transaction.archiveParent, lock.nextIndexPath) || + !validTransactionPaths(lock) + ) { + throw new Error('public evidence import lock fields are invalid'); + } +} + +function validTransactionPaths(record) { + const stage = /^\.economic-resilience-stage-(\d+)-([0-9a-f-]{36})$/u.exec( + path.basename(record.stagedPath ?? '') + ); + const next = /^\.economic-resilience-index-(\d+)-([0-9a-f-]{36})\.json$/u.exec( + path.basename(record.nextIndexPath ?? '') + ); + return Boolean( + stage && next && + Number(stage[1]) === record.pid && + Number(next[1]) === record.pid && + stage[2] === next[2] + ); +} + +function cleanupJournalFiles(journal, transaction) { + if (existsSync(journal.nextIndexPath)) unlinkSync(journal.nextIndexPath); + if (existsSync(journal.stagedPath)) { + removeValidatedTree(journal.stagedPath, transaction.archiveParent); + } + unlinkSync(transaction.journalPath); +} + +function cleanupPublishedTransaction(transaction) { + if (transaction.stageRoot && existsSync(transaction.stageRoot)) { + removeValidatedTree(transaction.stageRoot, transaction.archiveParent); + } + if (existsSync(transaction.journalPath)) unlinkSync(transaction.journalPath); +} + +function releaseLock(transaction) { + if (transaction.lockOwned && existsSync(transaction.lockPath)) { + unlinkSync(transaction.lockPath); + } + transaction.lockOwned = false; + for (const directory of [...transaction.createdParents].reverse()) { + removeEmptyDirectory(directory); + } +} + +function cleanupEmptyArchive(archiveRoot) { + removeEmptyDirectory(path.join(archiveRoot, 'source-tree')); + removeEmptyDirectory(archiveRoot); +} + +function removeValidatedTree(candidate, parent) { + const resolved = path.resolve(candidate); + if (!within(parent, resolved) || !path.basename(resolved).startsWith('.economic-resilience-stage-')) { + throw new Error('refused to clean an unexpected transaction path'); + } + const status = lstatSync(resolved); + if (!status.isDirectory() || status.isSymbolicLink()) { + throw new Error('transaction staging path must be a regular directory'); + } + assertSafeRemovalTree(resolved); + rmSync(resolved, { recursive: true, force: false }); +} + +function assertSafeRemovalTree(directory) { + for (const entry of readdirSync(directory)) { + const candidate = path.join(directory, entry); + const status = lstatSync(candidate); + if (status.isSymbolicLink()) { + throw new Error('transaction staging tree must not contain symbolic links'); + } + if (status.isDirectory()) assertSafeRemovalTree(candidate); + else if (!status.isFile() || status.nlink !== 1) { + throw new Error( + 'transaction staging tree entries must be single-link regular files or directories' + ); + } + } +} + +function copyExclusiveSingleLink(source, destination, label) { + readSecureSingleLinkFile(source, label); + copyFileSync(source, destination, constants.COPYFILE_EXCL); + readSecureSingleLinkFile(destination, `copied ${label}`); +} + +function writeExclusive(file, bytes, mode = 0o644) { + writeFileSync(file, bytes, { flag: 'wx', mode }); + readSecureSingleLinkFile(file, file); +} + +function readSecureSingleLinkFile(file, label) { + const status = lstatSync(file); + if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1) { + throw new Error(`${label} must be a regular non-symlink file with exactly one hard link`); + } + return readFileSync(file); +} + +function assertCurrentIndex(indexPath, expectedBytes) { + if (expectedBytes === null) { + if (existsSync(indexPath)) throw new Error('public archive index appeared during import'); + return; + } + const current = readSecureSingleLinkFile(indexPath, 'public archive index'); + if (!current.equals(expectedBytes)) { + throw new Error('public archive index changed during import'); + } +} + +function readEvidence(root, label) { + try { + return JSON.parse( + readSecureSingleLinkFile( + path.join(root, 'economic-resilience.json'), + `${label} economic evidence` + ).toString('utf8') + ); + } catch (error) { + throw new Error(`${label} economic evidence JSON is invalid: ${error.message}`); + } +} + +function assertSameBytes(left, right, message) { + if (!readSecureSingleLinkFile(left, left).equals(readSecureSingleLinkFile(right, right))) { + throw new Error(message); + } +} + +function assertRegularDirectory(directory, label) { + const status = lstatSync(directory); + if (!status.isDirectory() || status.isSymbolicLink()) { + throw new Error(`${label} must be a regular non-symlink directory`); + } +} + +function assertMissing(candidate, message) { + try { + lstatSync(candidate); + throw new Error(message); + } catch (error) { + if (error?.code !== 'ENOENT') throw error; + } +} + +function ensureDirectoryPath(directory) { + const requested = path.resolve(directory); + let cursor = requested; + const missing = []; + while (!existsSync(cursor)) { + missing.push(cursor); + const parent = path.dirname(cursor); + if (parent === cursor) throw new Error('archive parent cannot be resolved'); + cursor = parent; + } + assertRegularDirectory(cursor, 'archive existing ancestor'); + let current = realpathSync(cursor); + const created = []; + for (const component of path.relative(cursor, requested).split(path.sep).filter(Boolean)) { + current = path.join(current, component); + if (existsSync(current)) assertRegularDirectory(current, 'archive parent component'); + else { + mkdirSync(current); + created.push(current); + } + } + if (created.length !== missing.length) { + throw new Error('archive parent creation did not resolve the requested path'); + } + return created; +} + +function removeEmptyDirectory(directory) { + try { + rmdirSync(directory); + } catch (error) { + if (!['ENOENT', 'ENOTEMPTY'].includes(error?.code)) throw error; + } +} + +function processIsLive(pid) { + if (!Number.isSafeInteger(pid) || pid <= 0) return false; + try { + process.kill(pid, 0); + return true; + } catch (error) { + return error?.code === 'EPERM'; + } +} + +function triggerFailpoint(name) { + if ( + process.env.NODE_ENV === 'test' && + process.env.RNICK_PUBLIC_EVIDENCE_FAILPOINT === name + ) { + process.kill(process.pid, 'SIGKILL'); + } +} + +function within(parent, candidate) { + if (typeof candidate !== 'string') return false; + const relative = path.relative(parent, path.resolve(candidate)); + return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`); +} + +function digestOrNull(value) { + return value === null || /^[0-9a-f]{64}$/u.test(value ?? ''); +} + +function serializeJson(value) { + return Buffer.from(`${JSON.stringify(value, null, 2)}\n`); +} + +function sha256(value) { + return createHash('sha256').update(value).digest('hex'); +} + +function parseArgs(args) { + const parsed = {}; + const allowed = new Set(['runId', 'archiveRoot']); + const values = args.filter((value) => value !== '--'); + for (let index = 0; index < values.length; index += 2) { + const flag = values[index]; + const value = values[index + 1]; + if (!flag?.startsWith('--') || !value) { + throw new Error(`invalid argument: ${flag ?? ''}`); + } + const key = flag.slice(2).replace(/-([a-z])/gu, (_, letter) => letter.toUpperCase()); + if (!allowed.has(key)) throw new Error(`unsupported argument: ${flag}`); + if (parsed[key] !== undefined) throw new Error(`duplicate argument: ${flag}`); + parsed[key] = value; + } + return parsed; +} diff --git a/scripts/public-economic-resilience-evidence-core.mjs b/scripts/public-economic-resilience-evidence-core.mjs new file mode 100644 index 0000000..877bf31 --- /dev/null +++ b/scripts/public-economic-resilience-evidence-core.mjs @@ -0,0 +1,977 @@ +import { + createHash, +} from 'node:crypto'; +import { + existsSync, + lstatSync, + readFileSync, + readdirSync, + realpathSync, +} from 'node:fs'; +import path from 'node:path'; +import { isDeepStrictEqual } from 'node:util'; +import { + ECONOMIC_RESILIENCE_ASSET_FILES, + ECONOMIC_RESILIENCE_SCENARIO_ID, + inspectEconomicResilienceEvidence, +} from './economic-resilience-evidence-core.mjs'; +import { PORTABLE_DEMO_VISUAL_AGREEMENT_PROFILE } from './demo-visual-agreement-core.mjs'; +import { inspectPublicEconomicResilienceArtifactZip } from './public-economic-resilience-zip.mjs'; + +export const PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_SCHEMA_VERSION = 1; +export const PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ID = + 'economic-resilience-source-tree-v1'; +export const PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT = + 'website/public/evidence/economic-resilience'; +export const PUBLIC_ECONOMIC_RESILIENCE_INDEX_FILE = 'index.json'; +export const PUBLIC_ECONOMIC_RESILIENCE_CAPTURE_SET_FILE = 'capture-set.json'; +export const PUBLIC_ECONOMIC_RESILIENCE_RUN_METADATA_FILE = 'run-metadata.json'; +export const PUBLIC_ECONOMIC_RESILIENCE_ARTIFACT_METADATA_FILE = + 'artifact-metadata.json'; +export const PUBLIC_ECONOMIC_RESILIENCE_WORKFLOW = + '.github/workflows/demo-evidence.yml'; +export const PUBLIC_ECONOMIC_RESILIENCE_WORKFLOW_NAME = + 'Native Demo Evidence'; +export const PUBLIC_ECONOMIC_RESILIENCE_EVENT = 'workflow_dispatch'; +export const PUBLIC_ECONOMIC_RESILIENCE_BRANCH = 'master'; +export const PUBLIC_ECONOMIC_RESILIENCE_RUN_URL_PREFIX = + 'https://github.com/GGULBAE/react-native-image-compression-kit/actions/runs/'; + +const INDEX_FIELDS = ['archive', 'captures', 'schemaVersion'].sort(); +const INDEX_CAPTURE_FIELDS = ['captureSetPath', 'runId', 'sourceCommit'].sort(); +const CAPTURE_SET_FIELDS = [ + 'artifactMetadata', + 'claimBoundary', + 'platforms', + 'scenarioId', + 'schemaVersion', + 'sourceCommit', + 'status', + 'workflow', +].sort(); +const WORKFLOW_FIELDS = [ + 'eventName', + 'file', + 'headBranch', + 'headSha', + 'metadataFile', + 'metadataSha256', + 'ref', + 'runAttempt', + 'runId', + 'runUrl', + 'workflowName', +].sort(); +const RUN_METADATA_FIELDS = [ + 'attempt', + 'conclusion', + 'event', + 'headBranch', + 'headSha', + 'path', + 'runId', + 'url', + 'workflowName', +].sort(); +const PLATFORM_FIELDS = [ + 'artifactArchivePath', + 'artifactDigest', + 'artifactExpired', + 'artifactId', + 'artifactName', + 'artifactPath', + 'artifactSizeInBytes', +].sort(); +const ARTIFACT_METADATA_FIELDS = ['artifacts', 'runId', 'schemaVersion'].sort(); +const ARTIFACT_FIELDS = [ + 'archiveDownloadUrl', + 'digest', + 'expired', + 'id', + 'name', + 'platform', + 'sizeInBytes', +].sort(); +const CLAIM_BOUNDARY_FIELDS = [ + 'costSavingsClaim', + 'crossPlatformTimingComparison', + 'matchedTransferBaseline', + 'observation', + 'sourceOwnership', +].sort(); + +export function createEmptyPublicEconomicResilienceIndex() { + return { + schemaVersion: PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_SCHEMA_VERSION, + archive: PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ID, + captures: [], + }; +} + +export function buildPublicEconomicResilienceCaptureSet({ + sourceCommit, + runMetadata, + runMetadataBytes, + artifactMetadata, + artifactMetadataBytes, + androidEvidence, + iosEvidence, +}) { + const metadataErrors = inspectPublicEconomicResilienceRunMetadata( + runMetadata, + { sourceCommit } + ); + if (metadataErrors.length > 0) throw new Error(metadataErrors.join(' | ')); + if (!Buffer.isBuffer(runMetadataBytes)) { + throw new Error('public economic resilience run metadata bytes are required'); + } + const artifactErrors = inspectPublicEconomicResilienceArtifactMetadata( + artifactMetadata, + { runMetadata } + ); + if (artifactErrors.length > 0) throw new Error(artifactErrors.join(' | ')); + if (!Buffer.isBuffer(artifactMetadataBytes)) { + throw new Error('public economic resilience artifact metadata bytes are required'); + } + const artifactsByPlatform = Object.fromEntries( + artifactMetadata.artifacts.map((artifact) => [artifact.platform, artifact]) + ); + const captureSet = { + schemaVersion: PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_SCHEMA_VERSION, + status: 'passed', + scenarioId: ECONOMIC_RESILIENCE_SCENARIO_ID, + sourceCommit, + artifactMetadata: { + file: PUBLIC_ECONOMIC_RESILIENCE_ARTIFACT_METADATA_FILE, + sha256: sha256(artifactMetadataBytes), + }, + workflow: { + metadataFile: PUBLIC_ECONOMIC_RESILIENCE_RUN_METADATA_FILE, + metadataSha256: sha256(runMetadataBytes), + file: runMetadata.path, + workflowName: runMetadata.workflowName, + eventName: runMetadata.event, + headBranch: runMetadata.headBranch, + ref: `refs/heads/${runMetadata.headBranch}`, + headSha: runMetadata.headSha, + runId: runMetadata.runId, + runAttempt: runMetadata.attempt, + runUrl: runMetadata.url, + }, + platforms: { + android: capturePlatform('android', artifactsByPlatform.android), + ios: capturePlatform('ios', artifactsByPlatform.ios), + }, + claimBoundary: { + observation: 'source-to-output-byte-difference', + sourceOwnership: 'source-remains', + matchedTransferBaseline: null, + costSavingsClaim: null, + crossPlatformTimingComparison: null, + }, + }; + const errors = inspectPublicEconomicResilienceCaptureSet(captureSet, { + androidEvidence, + iosEvidence, + runMetadata, + runMetadataBytes, + artifactMetadata, + artifactMetadataBytes, + }); + if (errors.length > 0) throw new Error(errors.join(' | ')); + return captureSet; +} + +export function inspectPublicEconomicResilienceRunMetadata( + metadata, + { sourceCommit } = {} +) { + const errors = []; + if (!exactFields(metadata, RUN_METADATA_FIELDS)) { + errors.push('public economic resilience run metadata fields drifted'); + } + if ( + metadata?.event !== PUBLIC_ECONOMIC_RESILIENCE_EVENT || + metadata?.headBranch !== PUBLIC_ECONOMIC_RESILIENCE_BRANCH || + metadata?.conclusion !== 'success' || + metadata?.workflowName !== PUBLIC_ECONOMIC_RESILIENCE_WORKFLOW_NAME || + metadata?.path !== PUBLIC_ECONOMIC_RESILIENCE_WORKFLOW || + !fullSha(metadata?.headSha) || + (sourceCommit !== undefined && metadata?.headSha !== sourceCommit) || + !positiveInteger(metadata?.runId) || + !positiveInteger(metadata?.attempt) || + !validRunUrl(metadata?.url, metadata?.runId) + ) { + errors.push( + 'public economic resilience run metadata must identify a successful master workflow_dispatch run' + ); + } + return errors; +} + +export function inspectPublicEconomicResilienceArtifactMetadata( + metadata, + { runMetadata } = {} +) { + const errors = []; + if (!exactFields(metadata, ARTIFACT_METADATA_FIELDS)) { + errors.push('public economic resilience artifact metadata fields drifted'); + } + if ( + metadata?.schemaVersion !== 1 || + !positiveInteger(metadata?.runId) || + (runMetadata !== undefined && metadata?.runId !== runMetadata?.runId) + ) { + errors.push('public economic resilience artifact metadata run identity is invalid'); + } + if (!Array.isArray(metadata?.artifacts) || metadata.artifacts.length !== 2) { + errors.push('public economic resilience artifact metadata must contain two artifacts'); + return errors; + } + ['android', 'ios'].forEach((platform, index) => { + const artifact = metadata.artifacts[index]; + if (!exactFields(artifact, ARTIFACT_FIELDS)) { + errors.push(`public economic resilience ${platform} artifact fields drifted`); + } + if ( + artifact?.platform !== platform || + !positiveInteger(artifact?.id) || + artifact?.name !== + `native-demo-${platform}-${runMetadata?.headSha ?? ''}` || + !/^sha256:[0-9a-f]{64}$/u.test(artifact?.digest ?? '') || + artifact?.expired !== false || + !positiveInteger(artifact?.sizeInBytes) || + artifact.sizeInBytes > 64 * 1024 * 1024 || + artifact?.archiveDownloadUrl !== + `https://api.github.com/repos/GGULBAE/react-native-image-compression-kit/actions/artifacts/${artifact?.id}/zip` + ) { + errors.push(`public economic resilience ${platform} artifact identity is invalid`); + } + }); + return errors; +} + +export function appendPublicEconomicResilienceIndex(index, captureSet) { + const errors = inspectPublicEconomicResilienceIndex(index); + errors.push(...inspectPublicEconomicResilienceCaptureSet(captureSet)); + if (errors.length > 0) throw new Error(errors.join(' | ')); + if ( + index.captures.some( + ({ sourceCommit }) => sourceCommit === captureSet.sourceCommit + ) + ) { + throw new Error( + `public economic resilience capture already exists for ${captureSet.sourceCommit}` + ); + } + return { + ...structuredClone(index), + captures: [ + ...structuredClone(index.captures), + { + sourceCommit: captureSet.sourceCommit, + runId: captureSet.workflow.runId, + captureSetPath: + `source-tree/${captureSet.sourceCommit}/${PUBLIC_ECONOMIC_RESILIENCE_CAPTURE_SET_FILE}`, + }, + ], + }; +} + +export function inspectPublicEconomicResilienceIndex(index) { + const errors = []; + if (!exactFields(index, INDEX_FIELDS)) { + errors.push('public economic resilience index fields drifted'); + } + if ( + index?.schemaVersion !== + PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_SCHEMA_VERSION + ) { + errors.push('public economic resilience index schemaVersion is invalid'); + } + if (index?.archive !== PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ID) { + errors.push('public economic resilience archive identity drifted'); + } + if (!Array.isArray(index?.captures)) { + errors.push('public economic resilience index captures must be an array'); + return errors; + } + const seen = new Set(); + index.captures.forEach((entry, position) => { + if (!exactFields(entry, INDEX_CAPTURE_FIELDS)) { + errors.push(`public economic resilience index capture ${position + 1} fields drifted`); + } + if (!fullSha(entry?.sourceCommit)) { + errors.push(`public economic resilience index capture ${position + 1} SHA is invalid`); + } + if (!positiveInteger(entry?.runId)) { + errors.push(`public economic resilience index capture ${position + 1} runId is invalid`); + } + if ( + entry?.captureSetPath !== + `source-tree/${entry?.sourceCommit}/${PUBLIC_ECONOMIC_RESILIENCE_CAPTURE_SET_FILE}` + ) { + errors.push( + `public economic resilience index capture ${position + 1} path is invalid` + ); + } + if (seen.has(entry?.sourceCommit)) { + errors.push('public economic resilience index contains a duplicate source SHA'); + } + seen.add(entry?.sourceCommit); + }); + return errors; +} + +export function inspectPublicEconomicResilienceCaptureSet( + captureSet, + { + androidEvidence, + iosEvidence, + runMetadata, + runMetadataBytes, + artifactMetadata, + artifactMetadataBytes, + } = {} +) { + const errors = []; + if (!exactFields(captureSet, CAPTURE_SET_FIELDS)) { + errors.push('public economic resilience capture-set fields drifted'); + } + if ( + captureSet?.schemaVersion !== + PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_SCHEMA_VERSION || + captureSet?.status !== 'passed' + ) { + errors.push('public economic resilience capture-set schemaVersion/status is invalid'); + } + if (captureSet?.scenarioId !== ECONOMIC_RESILIENCE_SCENARIO_ID) { + errors.push('public economic resilience scenario identity drifted'); + } + if (!fullSha(captureSet?.sourceCommit)) { + errors.push('public economic resilience sourceCommit is invalid'); + } + if (!exactFields(captureSet?.workflow, WORKFLOW_FIELDS)) { + errors.push('public economic resilience workflow fields drifted'); + } + if ( + !exactFields(captureSet?.artifactMetadata, ['file', 'sha256']) || + captureSet?.artifactMetadata?.file !== + PUBLIC_ECONOMIC_RESILIENCE_ARTIFACT_METADATA_FILE || + !/^[0-9a-f]{64}$/u.test(captureSet?.artifactMetadata?.sha256 ?? '') + ) { + errors.push('public economic resilience artifact metadata binding is invalid'); + } + if ( + captureSet?.workflow?.file !== PUBLIC_ECONOMIC_RESILIENCE_WORKFLOW || + captureSet?.workflow?.workflowName !== + PUBLIC_ECONOMIC_RESILIENCE_WORKFLOW_NAME || + captureSet?.workflow?.eventName !== PUBLIC_ECONOMIC_RESILIENCE_EVENT || + captureSet?.workflow?.headBranch !== PUBLIC_ECONOMIC_RESILIENCE_BRANCH || + captureSet?.workflow?.ref !== + `refs/heads/${PUBLIC_ECONOMIC_RESILIENCE_BRANCH}` || + captureSet?.workflow?.headSha !== captureSet?.sourceCommit || + captureSet?.workflow?.metadataFile !== + PUBLIC_ECONOMIC_RESILIENCE_RUN_METADATA_FILE || + !/^[0-9a-f]{64}$/u.test(captureSet?.workflow?.metadataSha256 ?? '') || + !positiveInteger(captureSet?.workflow?.runId) || + !positiveInteger(captureSet?.workflow?.runAttempt) || + !validRunUrl( + captureSet?.workflow?.runUrl, + captureSet?.workflow?.runId + ) + ) { + errors.push('public economic resilience workflow identity is invalid'); + } + if (runMetadata !== undefined || runMetadataBytes !== undefined) { + errors.push( + ...inspectPublicEconomicResilienceRunMetadata(runMetadata, { + sourceCommit: captureSet?.sourceCommit, + }) + ); + if ( + !Buffer.isBuffer(runMetadataBytes) || + captureSet?.workflow?.metadataSha256 !== sha256(runMetadataBytes) || + captureSet?.workflow?.file !== runMetadata?.path || + captureSet?.workflow?.workflowName !== runMetadata?.workflowName || + captureSet?.workflow?.eventName !== runMetadata?.event || + captureSet?.workflow?.headBranch !== runMetadata?.headBranch || + captureSet?.workflow?.headSha !== runMetadata?.headSha || + captureSet?.workflow?.runId !== runMetadata?.runId || + captureSet?.workflow?.runAttempt !== runMetadata?.attempt || + captureSet?.workflow?.runUrl !== runMetadata?.url + ) { + errors.push('public economic resilience capture-set does not match run metadata'); + } + } + if (artifactMetadata !== undefined || artifactMetadataBytes !== undefined) { + errors.push( + ...inspectPublicEconomicResilienceArtifactMetadata(artifactMetadata, { + runMetadata, + }) + ); + if ( + !Buffer.isBuffer(artifactMetadataBytes) || + captureSet?.artifactMetadata?.sha256 !== sha256(artifactMetadataBytes) + ) { + errors.push('public economic resilience capture-set does not match artifact metadata'); + } + } + if (!exactFields(captureSet?.platforms, ['android', 'ios'])) { + errors.push('public economic resilience platform set drifted'); + } + for (const platform of ['android', 'ios']) { + if (!exactFields(captureSet?.platforms?.[platform], PLATFORM_FIELDS)) { + errors.push(`public economic resilience ${platform} platform fields drifted`); + } + const artifact = artifactMetadata?.artifacts?.find?.( + (candidate) => candidate.platform === platform + ); + if ( + captureSet?.platforms?.[platform]?.artifactPath !== platform || + captureSet?.platforms?.[platform]?.artifactArchivePath !== + `artifacts/${platform}.zip` || + !positiveInteger(captureSet?.platforms?.[platform]?.artifactId) || + captureSet?.platforms?.[platform]?.artifactName !== + `native-demo-${platform}-${captureSet?.sourceCommit}` || + !/^sha256:[0-9a-f]{64}$/u.test( + captureSet?.platforms?.[platform]?.artifactDigest ?? '' + ) || + captureSet?.platforms?.[platform]?.artifactExpired !== false || + !positiveInteger(captureSet?.platforms?.[platform]?.artifactSizeInBytes) + ) { + errors.push(`public economic resilience ${platform} artifact path is invalid`); + } + if ( + artifact !== undefined && + (captureSet?.platforms?.[platform]?.artifactId !== artifact.id || + captureSet?.platforms?.[platform]?.artifactName !== artifact.name || + captureSet?.platforms?.[platform]?.artifactDigest !== artifact.digest || + captureSet?.platforms?.[platform]?.artifactExpired !== artifact.expired || + captureSet?.platforms?.[platform]?.artifactSizeInBytes !== + artifact.sizeInBytes) + ) { + errors.push(`public economic resilience ${platform} artifact binding differs`); + } + } + if (!exactFields(captureSet?.claimBoundary, CLAIM_BOUNDARY_FIELDS)) { + errors.push('public economic resilience claim-boundary fields drifted'); + } + if ( + captureSet?.claimBoundary?.observation !== + 'source-to-output-byte-difference' || + captureSet?.claimBoundary?.sourceOwnership !== 'source-remains' || + captureSet?.claimBoundary?.matchedTransferBaseline !== null || + captureSet?.claimBoundary?.costSavingsClaim !== null || + captureSet?.claimBoundary?.crossPlatformTimingComparison !== null + ) { + errors.push('public economic resilience claim boundary is invalid'); + } + if (androidEvidence !== undefined || iosEvidence !== undefined) { + errors.push( + ...inspectCaptureEvidence( + captureSet, + androidEvidence, + 'android' + ), + ...inspectCaptureEvidence(captureSet, iosEvidence, 'ios') + ); + if ( + androidEvidence?.implementation?.version !== + iosEvidence?.implementation?.version || + androidEvidence?.fixture?.sha256 !== iosEvidence?.fixture?.sha256 || + androidEvidence?.fixture?.byteSize !== iosEvidence?.fixture?.byteSize + ) { + errors.push('public economic resilience platform source identities differ'); + } + } + return errors; +} + +export function inspectPublicEconomicResilienceArchive( + root, + { inspectPlatformArtifact = inspectEconomicResilienceEvidence } = {} +) { + const archiveRoot = path.resolve(root); + if (!existsSync(archiveRoot)) return emptyArchiveReport(); + const errors = []; + const rootEntries = readDirectoryEntries( + archiveRoot, + 'public economic resilience archive root', + errors + ); + if (rootEntries === null) return failedArchiveReport(errors); + const allowedRootEntries = new Set([ + PUBLIC_ECONOMIC_RESILIENCE_INDEX_FILE, + 'source-tree', + ]); + for (const entry of rootEntries) { + if (!allowedRootEntries.has(entry)) { + errors.push(`public economic resilience archive contains unexpected entry: ${entry}`); + } + } + + const indexPath = path.join( + archiveRoot, + PUBLIC_ECONOMIC_RESILIENCE_INDEX_FILE + ); + if (!existsSync(indexPath)) { + if (rootEntries.length === 0) return emptyArchiveReport(); + errors.push('public economic resilience archive index is missing'); + return failedArchiveReport(errors); + } + const index = readSecureJson( + indexPath, + 'public economic resilience index', + errors + ); + const indexErrors = + index === null ? [] : inspectPublicEconomicResilienceIndex(index); + errors.push(...indexErrors); + if (index === null || !Array.isArray(index.captures)) { + return failedArchiveReport(errors); + } + if (indexErrors.length > 0) return failedArchiveReport(errors); + + const sourceTree = path.join(archiveRoot, 'source-tree'); + let sourceTreeEntries = []; + if (existsSync(sourceTree)) { + const inspectedSourceTree = readDirectoryEntries( + sourceTree, + 'public economic resilience source-tree', + errors + ); + if (inspectedSourceTree === null) return failedArchiveReport(errors); + sourceTreeEntries = inspectedSourceTree; + } else if (index.captures.length > 0) { + errors.push('public economic resilience source-tree is missing'); + } + const indexedShas = index.captures.map(({ sourceCommit }) => sourceCommit); + if (!isDeepStrictEqual([...sourceTreeEntries].sort(), [...indexedShas].sort())) { + errors.push('public economic resilience index and source-tree contain orphan entries'); + } + + const captures = []; + for (const entry of index.captures) { + const captureRoot = path.join(sourceTree, entry.sourceCommit); + const capture = inspectCaptureDirectory({ + captureRoot, + indexEntry: entry, + inspectPlatformArtifact, + errors, + }); + if (capture !== null) captures.push(capture); + } + return { + schemaVersion: PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_SCHEMA_VERSION, + status: errors.length === 0 ? 'passed' : 'failed', + archiveState: + errors.length === 0 && captures.length === 0 ? 'empty' : + errors.length === 0 ? 'available' : 'invalid', + captureCount: errors.length === 0 ? captures.length : 0, + captures: errors.length === 0 ? captures : [], + error: errors.length > 0 ? errors.join(' | ') : null, + }; +} + +function inspectCaptureDirectory({ + captureRoot, + indexEntry, + inspectPlatformArtifact, + errors, +}) { + const entries = readDirectoryEntries( + captureRoot, + `public economic resilience capture ${indexEntry.sourceCommit}`, + errors + ); + if (entries === null) return null; + if ( + !isDeepStrictEqual( + [...entries].sort(), + [ + PUBLIC_ECONOMIC_RESILIENCE_CAPTURE_SET_FILE, + PUBLIC_ECONOMIC_RESILIENCE_RUN_METADATA_FILE, + PUBLIC_ECONOMIC_RESILIENCE_ARTIFACT_METADATA_FILE, + 'artifacts', + 'android', + 'ios', + ].sort() + ) + ) { + errors.push( + `public economic resilience capture ${indexEntry.sourceCommit} has an invalid entry set` + ); + } + const captureSet = readSecureJson( + path.join(captureRoot, PUBLIC_ECONOMIC_RESILIENCE_CAPTURE_SET_FILE), + `public economic resilience capture-set ${indexEntry.sourceCommit}`, + errors + ); + if (captureSet === null) return null; + const runMetadataPath = path.join( + captureRoot, + PUBLIC_ECONOMIC_RESILIENCE_RUN_METADATA_FILE + ); + const runMetadataBytes = readSecureFile( + runMetadataPath, + `public economic resilience run metadata ${indexEntry.sourceCommit}`, + errors + ); + let runMetadata = null; + if (runMetadataBytes !== null) { + try { + runMetadata = JSON.parse(runMetadataBytes.toString('utf8')); + } catch (error) { + errors.push( + `public economic resilience run metadata JSON is invalid: ${error.message}` + ); + } + } + const artifactMetadataPath = path.join( + captureRoot, + PUBLIC_ECONOMIC_RESILIENCE_ARTIFACT_METADATA_FILE + ); + const artifactMetadataBytes = readSecureFile( + artifactMetadataPath, + `public economic resilience artifact metadata ${indexEntry.sourceCommit}`, + errors + ); + let artifactMetadata = null; + if (artifactMetadataBytes !== null) { + try { + artifactMetadata = JSON.parse(artifactMetadataBytes.toString('utf8')); + } catch (error) { + errors.push( + `public economic resilience artifact metadata JSON is invalid: ${error.message}` + ); + } + } + + const platformEvidence = {}; + const artifactMetadataByPlatform = new Map( + (artifactMetadata?.artifacts ?? []).map((artifact) => [ + artifact.platform, + artifact, + ]) + ); + const artifactsRoot = path.join(captureRoot, 'artifacts'); + const artifactEntries = readDirectoryEntries( + artifactsRoot, + `public economic resilience retained artifacts ${indexEntry.sourceCommit}`, + errors + ); + if ( + artifactEntries !== null && + !isDeepStrictEqual(artifactEntries, ['android.zip', 'ios.zip']) + ) { + errors.push('public economic resilience retained artifact ZIP set drifted'); + } + for (const platform of ['android', 'ios']) { + const artifactRoot = path.join(captureRoot, platform); + if (!inspectExactArtifactFiles(artifactRoot, platform, errors)) continue; + const evidence = readSecureJson( + path.join(artifactRoot, 'economic-resilience.json'), + `public economic resilience ${platform} evidence`, + errors + ); + if (evidence === null) continue; + const report = inspectPlatformArtifact(artifactRoot, evidence); + if (report?.status !== 'passed') { + errors.push( + `public economic resilience ${platform} artifact failed: ${report?.error ?? 'unknown error'}` + ); + } + platformEvidence[platform] = evidence; + + const retainedZip = path.join(artifactsRoot, `${platform}.zip`); + const retainedZipBytes = readSecureFile( + retainedZip, + `public economic resilience retained ${platform} artifact ZIP`, + errors + ); + const artifact = artifactMetadataByPlatform.get(platform); + if ( + retainedZipBytes !== null && + (retainedZipBytes.length !== artifact?.sizeInBytes || + `sha256:${sha256(retainedZipBytes)}` !== artifact?.digest) + ) { + errors.push( + `public economic resilience retained ${platform} artifact ZIP binding differs` + ); + } + if (retainedZipBytes !== null) { + try { + const inspectedZip = inspectPublicEconomicResilienceArtifactZip(retainedZip); + for (const asset of ECONOMIC_RESILIENCE_ASSET_FILES) { + const archivedAsset = readSecureFile( + path.join(artifactRoot, asset), + `public economic resilience ${platform} archived asset ${asset}`, + errors + ); + const zipAsset = inspectedZip.members.get( + `economic-resilience/${asset}` + ); + if (archivedAsset && !archivedAsset.equals(zipAsset)) { + errors.push( + `public economic resilience ${platform} archived asset differs from retained ZIP: ${asset}` + ); + } + } + } catch (error) { + errors.push( + `public economic resilience retained ${platform} artifact ZIP failed: ${error.message}` + ); + } + } + } + errors.push( + ...inspectPublicEconomicResilienceCaptureSet(captureSet, { + androidEvidence: platformEvidence.android, + iosEvidence: platformEvidence.ios, + runMetadata, + runMetadataBytes, + artifactMetadata, + artifactMetadataBytes, + }) + ); + if ( + captureSet?.sourceCommit !== indexEntry.sourceCommit || + captureSet?.workflow?.runId !== indexEntry.runId || + indexEntry.captureSetPath !== + `source-tree/${indexEntry.sourceCommit}/${PUBLIC_ECONOMIC_RESILIENCE_CAPTURE_SET_FILE}` + ) { + errors.push('public economic resilience index does not match its capture-set'); + } + + if (platformEvidence.android && platformEvidence.ios) { + const androidSource = readSecureFile( + path.join(captureRoot, 'android', 'source.jpg'), + 'public economic resilience Android source', + errors + ); + const iosSource = readSecureFile( + path.join(captureRoot, 'ios', 'source.jpg'), + 'public economic resilience iOS source', + errors + ); + const androidFixture = readSecureFile( + path.join(captureRoot, 'android', 'fixture-manifest.json'), + 'public economic resilience Android fixture manifest', + errors + ); + const iosFixture = readSecureFile( + path.join(captureRoot, 'ios', 'fixture-manifest.json'), + 'public economic resilience iOS fixture manifest', + errors + ); + if ( + androidSource && iosSource && !androidSource.equals(iosSource) + ) { + errors.push('public economic resilience platform source bytes differ'); + } + if ( + androidFixture && iosFixture && !androidFixture.equals(iosFixture) + ) { + errors.push('public economic resilience platform fixture manifests differ'); + } + } + + return { + sourceCommit: captureSet?.sourceCommit, + runId: captureSet?.workflow?.runId, + runAttempt: captureSet?.workflow?.runAttempt, + platforms: { + android: { + artifactPath: `source-tree/${captureSet?.sourceCommit}/android`, + artifactArchivePath: + `source-tree/${captureSet?.sourceCommit}/artifacts/android.zip`, + }, + ios: { + artifactPath: `source-tree/${captureSet?.sourceCommit}/ios`, + artifactArchivePath: + `source-tree/${captureSet?.sourceCommit}/artifacts/ios.zip`, + }, + }, + }; +} + +function inspectCaptureEvidence(captureSet, evidence, platform) { + const errors = []; + if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence)) { + return [`public economic resilience ${platform} evidence is missing`]; + } + if ( + evidence.sourceCommit !== captureSet.sourceCommit || + evidence.runId !== captureSet.workflow.runId || + evidence.runAttempt !== captureSet.workflow.runAttempt || + evidence.runUrl !== captureSet.workflow.runUrl || + evidence.environment?.platform !== platform + ) { + errors.push(`public economic resilience ${platform} run identity differs`); + } + if ( + evidence.scenarioId !== ECONOMIC_RESILIENCE_SCENARIO_ID || + evidence.implementation?.buildSource !== 'checked-out-source-tree' || + evidence.fixture?.remainsAfterRun !== true || + evidence.economics?.boundary !== 'source-to-output-observation' || + evidence.economics?.sourceOwnership !== 'source-remains' || + evidence.economics?.matchedTransferBaseline !== null || + evidence.economics?.costSavingsClaim !== null || + evidence.cleanup?.attemptedPackageOutputs !== 12 || + evidence.cleanup?.removedPackageOutputs !== 12 || + evidence.cleanup?.residualPackageOutputs !== 0 || + evidence.cleanup?.residualPackageOutputBytes !== 0 || + evidence.visualAgreement?.schemaVersion !== 3 || + evidence.visualAgreement?.comparisonProfile !== + PORTABLE_DEMO_VISUAL_AGREEMENT_PROFILE + ) { + errors.push(`public economic resilience ${platform} claim boundary differs`); + } + return errors; +} + +function inspectExactArtifactFiles(root, platform, errors) { + const initialErrorCount = errors.length; + const entries = readDirectoryEntries( + root, + `public economic resilience ${platform} artifact`, + errors + ); + if (entries === null) return false; + if (!isDeepStrictEqual([...entries].sort(), [...ECONOMIC_RESILIENCE_ASSET_FILES])) { + errors.push( + `public economic resilience ${platform} artifact must contain exactly six assets` + ); + return false; + } + for (const entry of entries) { + const candidate = path.join(root, entry); + try { + const status = lstatSync(candidate); + if ( + !status.isFile() || + status.isSymbolicLink() || + status.nlink !== 1 + ) { + errors.push( + `public economic resilience ${platform} asset must be a regular non-symlink file with exactly one hard link: ${entry}` + ); + } + } catch (error) { + errors.push( + `public economic resilience ${platform} asset cannot be inspected: ${entry}: ${error.message}` + ); + } + } + return errors.length === initialErrorCount; +} + +function readDirectoryEntries(directory, label, errors) { + try { + const status = lstatSync(directory); + if (!status.isDirectory() || status.isSymbolicLink()) { + errors.push(`${label} must be a regular non-symlink directory`); + return null; + } + realpathSync(directory); + return readdirSync(directory).sort(); + } catch (error) { + errors.push(`${label} cannot be inspected: ${error.message}`); + return null; + } +} + +function readSecureJson(file, label, errors) { + const contents = readSecureFile(file, label, errors); + if (contents === null) return null; + try { + return JSON.parse(contents.toString('utf8')); + } catch (error) { + errors.push(`${label} JSON is invalid: ${error.message}`); + return null; + } +} + +function readSecureFile(file, label, errors) { + try { + const status = lstatSync(file); + if ( + !status.isFile() || + status.isSymbolicLink() || + status.nlink !== 1 + ) { + errors.push( + `${label} must be a regular non-symlink file with exactly one hard link` + ); + return null; + } + return readFileSync(file); + } catch (error) { + errors.push(`${label} cannot be read: ${error.message}`); + return null; + } +} + +function emptyArchiveReport() { + return { + schemaVersion: PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_SCHEMA_VERSION, + status: 'passed', + archiveState: 'empty', + captureCount: 0, + captures: [], + error: null, + }; +} + +function failedArchiveReport(errors) { + return { + schemaVersion: PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_SCHEMA_VERSION, + status: 'failed', + archiveState: 'invalid', + captureCount: 0, + captures: [], + error: errors.join(' | '), + }; +} + +function exactFields(value, expected) { + return Boolean( + value && + typeof value === 'object' && + !Array.isArray(value) && + isDeepStrictEqual(Object.keys(value).sort(), [...expected].sort()) + ); +} + +function fullSha(value) { + return /^[0-9a-f]{40}$/u.test(value ?? ''); +} + +function positiveInteger(value) { + return Number.isSafeInteger(value) && value > 0; +} + +function validRunUrl(value, runId) { + return ( + typeof value === 'string' && + value === `${PUBLIC_ECONOMIC_RESILIENCE_RUN_URL_PREFIX}${runId}` && + Number(value.split('/').at(-1)) === runId + ); +} + +function capturePlatform(platform, artifact) { + return { + artifactPath: platform, + artifactArchivePath: `artifacts/${platform}.zip`, + artifactId: artifact.id, + artifactName: artifact.name, + artifactDigest: artifact.digest, + artifactExpired: artifact.expired, + artifactSizeInBytes: artifact.sizeInBytes, + }; +} + +function sha256(value) { + return createHash('sha256').update(value).digest('hex'); +} diff --git a/scripts/public-economic-resilience-evolution-core.mjs b/scripts/public-economic-resilience-evolution-core.mjs new file mode 100644 index 0000000..0c2e9be --- /dev/null +++ b/scripts/public-economic-resilience-evolution-core.mjs @@ -0,0 +1,49 @@ +import { isDeepStrictEqual } from 'node:util'; +import { + inspectPublicEconomicResilienceIndex, +} from './public-economic-resilience-evidence-core.mjs'; + +export function inspectPublicEconomicResilienceEvolution({ + baseArchivePresent, + currentArchivePresent, + baseIndex, + currentIndex, + baseFiles, + currentFiles, +}) { + const errors = []; + if (baseArchivePresent && !currentArchivePresent) { + errors.push('public economic resilience archive was removed after publication'); + return errors; + } + if (!baseArchivePresent) return errors; + errors.push( + ...inspectPublicEconomicResilienceIndex(baseIndex).map( + (error) => `base archive: ${error}` + ), + ...inspectPublicEconomicResilienceIndex(currentIndex).map( + (error) => `current archive: ${error}` + ) + ); + if (errors.length > 0) return errors; + if (currentIndex.captures.length < baseIndex.captures.length) { + errors.push('public economic resilience capture history was truncated'); + } else if ( + !isDeepStrictEqual( + currentIndex.captures.slice(0, baseIndex.captures.length), + baseIndex.captures + ) + ) { + errors.push('public economic resilience capture history is not an exact ordered prefix'); + } + for (const [relativePath, baseBytes] of baseFiles) { + if (relativePath === 'index.json') continue; + const currentBytes = currentFiles.get(relativePath); + if (!Buffer.isBuffer(currentBytes)) { + errors.push(`published public economic resilience file was removed: ${relativePath}`); + } else if (!Buffer.from(baseBytes).equals(currentBytes)) { + errors.push(`published public economic resilience file changed: ${relativePath}`); + } + } + return errors; +} diff --git a/scripts/public-economic-resilience-github.mjs b/scripts/public-economic-resilience-github.mjs new file mode 100644 index 0000000..315ac8c --- /dev/null +++ b/scripts/public-economic-resilience-github.mjs @@ -0,0 +1,245 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { + lstatSync, + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import os from 'node:os'; +import path from 'node:path'; +import { ECONOMIC_RESILIENCE_ASSET_FILES } from './economic-resilience-evidence-core.mjs'; +import { + PUBLIC_ECONOMIC_RESILIENCE_BRANCH, + inspectPublicEconomicResilienceArtifactMetadata, + inspectPublicEconomicResilienceRunMetadata, +} from './public-economic-resilience-evidence-core.mjs'; +import { + PUBLIC_ECONOMIC_RESILIENCE_MAX_ZIP_BYTES, + inspectPublicEconomicResilienceArtifactZip, +} from './public-economic-resilience-zip.mjs'; + +const REPOSITORY_API = + 'repos/GGULBAE/react-native-image-compression-kit'; +const ARTIFACT_METADATA_SCHEMA_VERSION = 1; + +export function acquirePublicEconomicResilienceRun( + runId, + { command = spawnSync } = {} +) { + if (!Number.isSafeInteger(runId) || runId <= 0) { + throw new Error('--run-id must be a positive safe integer'); + } + const temporaryRoot = mkdtempSync( + path.join(os.tmpdir(), 'rnick-public-economic-acquisition-') + ); + try { + const rawRun = requestJson( + command, + `${REPOSITORY_API}/actions/runs/${runId}`, + 'GitHub workflow run' + ); + const runMetadata = normalizeRunMetadata(rawRun); + const runErrors = inspectPublicEconomicResilienceRunMetadata(runMetadata, { + sourceCommit: runMetadata.headSha, + }); + if (runMetadata.runId !== runId) { + runErrors.push('GitHub workflow run ID differs from --run-id'); + } + if (runErrors.length > 0) throw new Error(runErrors.join(' | ')); + + const rawArtifacts = requestJson( + command, + `${REPOSITORY_API}/actions/runs/${runId}/artifacts?per_page=100`, + 'GitHub workflow artifacts' + ); + const artifacts = inspectAndNormalizeArtifacts(rawArtifacts, runMetadata); + const runMetadataBytes = serializeJson(runMetadata); + const artifactMetadata = { + schemaVersion: ARTIFACT_METADATA_SCHEMA_VERSION, + runId, + artifacts, + }; + const artifactMetadataBytes = serializeJson(artifactMetadata); + const platforms = {}; + + for (const artifact of artifacts) { + const zipBytes = requestBytes( + command, + `${REPOSITORY_API}/actions/artifacts/${artifact.id}/zip`, + `GitHub ${artifact.platform} artifact ZIP` + ); + if ( + zipBytes.length !== artifact.sizeInBytes || + zipBytes.length === 0 || + zipBytes.length > PUBLIC_ECONOMIC_RESILIENCE_MAX_ZIP_BYTES + ) { + throw new Error( + `GitHub ${artifact.platform} artifact ZIP size differs from API metadata` + ); + } + const digest = `sha256:${sha256(zipBytes)}`; + if (digest !== artifact.digest) { + throw new Error( + `GitHub ${artifact.platform} artifact ZIP digest differs from API metadata` + ); + } + const zipFile = path.join( + temporaryRoot, + `${artifact.platform}-${artifact.id}-${randomUUID()}.zip` + ); + writeFileSync(zipFile, zipBytes, { flag: 'wx', mode: 0o600 }); + const destination = path.join(temporaryRoot, artifact.platform); + mkdirSync(destination); + extractEconomicArtifact(command, zipFile, destination, artifact.platform); + platforms[artifact.platform] = { + artifactRoot: destination, + zipFile, + metadata: artifact, + }; + } + + return { + sourceCommit: runMetadata.headSha, + runMetadata, + runMetadataBytes, + artifactMetadata, + artifactMetadataBytes, + platforms, + cleanup() { + rmSync(temporaryRoot, { recursive: true, force: false }); + }, + }; + } catch (error) { + rmSync(temporaryRoot, { recursive: true, force: false }); + throw error; + } +} + +function normalizeRunMetadata(raw) { + return { + event: raw?.event, + headBranch: raw?.head_branch, + headSha: raw?.head_sha, + conclusion: raw?.conclusion, + workflowName: raw?.name, + path: raw?.path, + runId: raw?.id, + attempt: raw?.run_attempt, + url: raw?.html_url, + }; +} + +function inspectAndNormalizeArtifacts(raw, runMetadata) { + if ( + !raw || + typeof raw !== 'object' || + Array.isArray(raw) || + raw.total_count !== 2 || + !Array.isArray(raw.artifacts) || + raw.artifacts.length !== 2 + ) { + throw new Error('GitHub workflow must expose exactly two native demo artifacts'); + } + const normalized = raw.artifacts + .map((artifact) => { + const prefix = 'native-demo-'; + const suffix = `-${runMetadata.headSha}`; + const platform = artifact?.name?.startsWith(prefix) && + artifact.name.endsWith(suffix) + ? artifact.name.slice(prefix.length, -suffix.length) + : null; + if ( + !['android', 'ios'].includes(platform) || + artifact?.workflow_run?.id !== runMetadata.runId || + artifact?.workflow_run?.head_branch !== PUBLIC_ECONOMIC_RESILIENCE_BRANCH || + artifact?.workflow_run?.head_sha !== runMetadata.headSha + ) { + throw new Error('GitHub artifact workflow identity is invalid'); + } + return { + platform, + id: artifact.id, + name: artifact.name, + digest: artifact.digest, + expired: artifact.expired, + sizeInBytes: artifact.size_in_bytes, + archiveDownloadUrl: artifact.archive_download_url, + }; + }) + .sort((left, right) => left.platform.localeCompare(right.platform)); + const metadata = { + schemaVersion: ARTIFACT_METADATA_SCHEMA_VERSION, + runId: runMetadata.runId, + artifacts: normalized, + }; + const errors = inspectPublicEconomicResilienceArtifactMetadata(metadata, { + runMetadata, + }); + if (errors.length > 0) throw new Error(errors.join(' | ')); + return normalized; +} + +function extractEconomicArtifact(command, zipFile, destination, platform) { + const { members } = inspectPublicEconomicResilienceArtifactZip(zipFile, { + command, + }); + for (const asset of ECONOMIC_RESILIENCE_ASSET_FILES) { + const member = `economic-resilience/${asset}`; + const extracted = members.get(member); + const destinationFile = path.join(destination, asset); + writeFileSync(destinationFile, extracted, { + flag: 'wx', + mode: 0o644, + }); + const status = lstatSync(destinationFile); + if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1) { + throw new Error( + `${platform} acquired asset is not a single-link regular file: ${asset}` + ); + } + } +} + +function requestJson(command, endpoint, label) { + const bytes = request(command, endpoint, label, 'utf8'); + try { + return JSON.parse(bytes); + } catch (error) { + throw new Error(`${label} returned invalid JSON: ${error.message}`); + } +} + +function requestBytes(command, endpoint, label) { + const result = request(command, endpoint, label, null); + return Buffer.isBuffer(result) ? result : Buffer.from(result); +} + +function request(command, endpoint, label, encoding) { + const result = command('gh', ['api', endpoint], { + encoding, + maxBuffer: PUBLIC_ECONOMIC_RESILIENCE_MAX_ZIP_BYTES + 1024 * 1024, + timeout: 60_000, + }); + assertCommand(result, label); + return result.stdout; +} + +function assertCommand(result, label) { + if (result.error) throw result.error; + if (result.status !== 0) { + const stderr = Buffer.isBuffer(result.stderr) + ? result.stderr.toString('utf8') + : String(result.stderr ?? ''); + throw new Error(`${label} failed: ${stderr.trim() || `exit ${result.status}`}`); + } +} + +function serializeJson(value) { + return Buffer.from(`${JSON.stringify(value, null, 2)}\n`); +} + +function sha256(value) { + return createHash('sha256').update(value).digest('hex'); +} diff --git a/scripts/public-economic-resilience-zip.mjs b/scripts/public-economic-resilience-zip.mjs new file mode 100644 index 0000000..0620bdb --- /dev/null +++ b/scripts/public-economic-resilience-zip.mjs @@ -0,0 +1,141 @@ +import { spawnSync } from 'node:child_process'; +import { isDeepStrictEqual } from 'node:util'; +import { ECONOMIC_RESILIENCE_ASSET_FILES } from './economic-resilience-evidence-core.mjs'; + +export const PUBLIC_ECONOMIC_RESILIENCE_MAX_ZIP_BYTES = 64 * 1024 * 1024; +export const PUBLIC_ECONOMIC_RESILIENCE_MAX_MEMBER_BYTES = 32 * 1024 * 1024; +export const PUBLIC_ECONOMIC_RESILIENCE_MAX_UNCOMPRESSED_BYTES = + 128 * 1024 * 1024; + +export const PUBLIC_ECONOMIC_RESILIENCE_NATIVE_ARTIFACT_MEMBERS = Object.freeze([ + 'benchmark-comparison.json', + 'benchmark.json', + 'comparison-plan.json', + ...ECONOMIC_RESILIENCE_ASSET_FILES.map( + (file) => `economic-resilience/${file}` + ), + 'manifest.json', + 'output.jpg', + 'recording.mp4', + 'screen.png', + 'source.jpg', +].sort()); + +/** + * Validates the complete, exact native artifact member inventory and boundedly + * extracts every member. `unzip -p` exits non-zero on CRC errors, so this also + * checks CRC without an unbounded whole-archive extraction. + */ +export function inspectPublicEconomicResilienceArtifactZip( + zipFile, + { command = spawnSync } = {} +) { + const listing = command('zipinfo', ['-l', zipFile], { + encoding: 'utf8', + maxBuffer: 2 * 1024 * 1024, + timeout: 10_000, + }); + assertCommand(listing, 'artifact ZIP member listing'); + const entries = parseZipinfoListing(listing.stdout); + const names = entries.map(({ name }) => name); + if ( + new Set(names).size !== names.length || + !isDeepStrictEqual([...names].sort(), [ + ...PUBLIC_ECONOMIC_RESILIENCE_NATIVE_ARTIFACT_MEMBERS, + ]) + ) { + throw new Error('artifact ZIP member shape is invalid'); + } + let totalUncompressedBytes = 0; + for (const entry of entries) { + if (entry.mode[0] !== '-') { + throw new Error(`artifact ZIP member is not a regular file: ${entry.name}`); + } + if ( + !Number.isSafeInteger(entry.uncompressedBytes) || + entry.uncompressedBytes <= 0 || + entry.uncompressedBytes > PUBLIC_ECONOMIC_RESILIENCE_MAX_MEMBER_BYTES + ) { + throw new Error(`artifact ZIP member size is invalid: ${entry.name}`); + } + totalUncompressedBytes += entry.uncompressedBytes; + } + if (totalUncompressedBytes > PUBLIC_ECONOMIC_RESILIENCE_MAX_UNCOMPRESSED_BYTES) { + throw new Error('artifact ZIP uncompressed size exceeds the public evidence cap'); + } + + const members = new Map(); + for (const entry of entries) { + const extracted = command('unzip', ['-p', zipFile, entry.name], { + encoding: null, + maxBuffer: Math.min( + PUBLIC_ECONOMIC_RESILIENCE_MAX_MEMBER_BYTES + 64 * 1024, + entry.uncompressedBytes + 64 * 1024 + ), + timeout: 30_000, + }); + assertCommand(extracted, `artifact ZIP member ${entry.name}`); + if ( + !Buffer.isBuffer(extracted.stdout) || + extracted.stdout.length !== entry.uncompressedBytes + ) { + throw new Error(`artifact ZIP member size differs after extraction: ${entry.name}`); + } + members.set(entry.name, extracted.stdout); + } + return { + members, + inventory: entries.map(({ name, uncompressedBytes }) => ({ + name, + uncompressedBytes, + })), + totalUncompressedBytes, + }; +} + +function parseZipinfoListing(output) { + if (typeof output !== 'string') { + throw new Error('artifact ZIP member listing returned non-text output'); + } + const entries = []; + for (const line of output.split(/\r?\n/u)) { + if (!/^[dl-][rwxStTs-]{9}\s/u.test(line)) continue; + const fields = line.trim().split(/\s+/u); + if (fields.length < 10) { + throw new Error('artifact ZIP member listing is malformed'); + } + const uncompressedBytes = Number(fields[3]); + const compressedBytes = Number(fields[5]); + const name = fields.slice(9).join(' '); + if ( + !name || + name.startsWith('/') || + name.includes('\\') || + name.split('/').includes('..') || + !Number.isSafeInteger(compressedBytes) || + compressedBytes < 0 + ) { + throw new Error('artifact ZIP contains an unsafe member'); + } + entries.push({ + mode: fields[0], + uncompressedBytes, + compressedBytes, + name, + }); + } + if (entries.length === 0) { + throw new Error('artifact ZIP member listing contains no regular entries'); + } + return entries; +} + +function assertCommand(result, label) { + if (result?.error) throw result.error; + if (result?.status !== 0) { + const stderr = Buffer.isBuffer(result?.stderr) + ? result.stderr.toString('utf8') + : String(result?.stderr ?? ''); + throw new Error(`${label} failed: ${stderr.trim() || `exit ${result?.status}`}`); + } +} diff --git a/scripts/verify-public-economic-resilience-evidence.mjs b/scripts/verify-public-economic-resilience-evidence.mjs new file mode 100644 index 0000000..6766b51 --- /dev/null +++ b/scripts/verify-public-economic-resilience-evidence.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT, + inspectPublicEconomicResilienceArchive, +} from './public-economic-resilience-evidence-core.mjs'; +import { replayEconomicResilienceArtifact } from './economic-resilience-replay.mjs'; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const options = parseArgs(process.argv.slice(2)); +const archiveRoot = path.resolve( + options.archiveRoot ?? + path.join(repositoryRoot, PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT) +); +const structural = inspectPublicEconomicResilienceArchive(archiveRoot); +const replay = []; +const errors = []; +if (structural.status === 'passed') { + for (const capture of structural.captures) { + for (const platform of ['android', 'ios']) { + const artifactRoot = path.join( + archiveRoot, + capture.platforms[platform].artifactPath + ); + try { + const report = replayEconomicResilienceArtifact(artifactRoot); + replay.push({ + sourceCommit: capture.sourceCommit, + platform, + status: 'passed', + measurementMode: report.replay.measurementMode, + measurementTolerance: report.replay.measurementTolerance, + }); + } catch (error) { + errors.push( + `${capture.sourceCommit} ${platform}: ${error.message}` + ); + } + } + } +} +const report = { + ...structural, + status: + structural.status === 'passed' && errors.length === 0 ? 'passed' : 'failed', + replay, + error: + structural.error ?? (errors.length > 0 ? errors.join(' | ') : null), +}; +process.stdout.write(`${JSON.stringify(report)}\n`); +if (report.status !== 'passed') process.exitCode = 1; + +function parseArgs(args) { + const parsed = {}; + const values = args.filter((value) => value !== '--'); + for (let index = 0; index < values.length; index += 2) { + const flag = values[index]; + const value = values[index + 1]; + if (flag !== '--archive-root' || !value) { + throw new Error(`invalid argument: ${flag ?? ''}`); + } + if (parsed.archiveRoot !== undefined) { + throw new Error('duplicate argument: --archive-root'); + } + parsed.archiveRoot = value; + } + return parsed; +} diff --git a/scripts/verify-public-economic-resilience-evolution.mjs b/scripts/verify-public-economic-resilience-evolution.mjs new file mode 100644 index 0000000..dae1dd6 --- /dev/null +++ b/scripts/verify-public-economic-resilience-evolution.mjs @@ -0,0 +1,181 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import { existsSync, lstatSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT, + PUBLIC_ECONOMIC_RESILIENCE_INDEX_FILE, + inspectPublicEconomicResilienceArchive, +} from './public-economic-resilience-evidence-core.mjs'; +import { inspectPublicEconomicResilienceEvolution } from './public-economic-resilience-evolution-core.mjs'; +import { acquirePublicEconomicResilienceRun } from './public-economic-resilience-github.mjs'; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const options = parseArgs(process.argv.slice(2)); +if (!options.base) throw new Error('--base is required'); +if (/^0{40}$/u.test(options.base)) { + process.stdout.write(`${JSON.stringify({status: 'passed', evolutionState: 'skipped-zero-base'})}\n`); + process.exit(0); +} +if (!/^[0-9a-f]{40}$/u.test(options.base)) { + throw new Error('--base must be a full lowercase commit SHA'); +} +mustGit(['cat-file', '-e', `${options.base}^{commit}`], 'comparison base commit'); + +const archiveRoot = path.join(repositoryRoot, PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT); +const currentReport = inspectPublicEconomicResilienceArchive(archiveRoot); +if (currentReport.status !== 'passed') throw new Error(currentReport.error); +const baseEntries = readBaseEntries(options.base); +const baseArchivePresent = baseEntries.length > 0; +const currentIndexPath = path.join(archiveRoot, PUBLIC_ECONOMIC_RESILIENCE_INDEX_FILE); +const currentArchivePresent = existsSync(currentIndexPath); +const baseFiles = new Map(); +for (const entry of baseEntries) { + if (entry.mode !== '100644' && entry.mode !== '100755') { + throw new Error(`base public economic resilience entry is not a regular file: ${entry.path}`); + } + baseFiles.set(entry.relativePath, mustGitBytes(['show', `${options.base}:${entry.path}`], entry.path)); +} +const currentFiles = currentArchivePresent + ? readCurrentFiles(archiveRoot, baseFiles.keys()) + : new Map(); +const baseIndexBytes = baseFiles.get(PUBLIC_ECONOMIC_RESILIENCE_INDEX_FILE); +if (baseArchivePresent && !baseIndexBytes) { + throw new Error('base public economic resilience archive index is missing'); +} +const baseIndex = baseIndexBytes + ? JSON.parse(baseIndexBytes.toString('utf8')) + : { schemaVersion: 1, archive: 'economic-resilience-source-tree-v1', captures: [] }; +const currentIndex = currentArchivePresent + ? JSON.parse(readSecureFile(currentIndexPath, 'current archive index').toString('utf8')) + : { schemaVersion: 1, archive: 'economic-resilience-source-tree-v1', captures: [] }; +const errors = inspectPublicEconomicResilienceEvolution({ + baseArchivePresent, + currentArchivePresent, + baseIndex, + currentIndex, + baseFiles, + currentFiles, +}); +if (errors.length === 0) { + for (const entry of currentIndex.captures.slice(baseIndex.captures.length)) { + verifyNewCaptureAgainstGitHub(entry, archiveRoot); + } +} +const report = { + status: errors.length === 0 ? 'passed' : 'failed', + evolutionState: baseArchivePresent ? 'compared' : 'first-publication', + base: options.base, + preservedFileCount: baseFiles.size - (baseFiles.has('index.json') ? 1 : 0), + previousCaptureCount: baseIndex.captures.length, + currentCaptureCount: currentIndex.captures.length, + error: errors.length > 0 ? errors.join(' | ') : null, +}; +process.stdout.write(`${JSON.stringify(report)}\n`); +if (errors.length > 0) process.exitCode = 1; + +function verifyNewCaptureAgainstGitHub(entry, archiveRoot) { + const acquisition = acquirePublicEconomicResilienceRun(entry.runId); + try { + if (acquisition.sourceCommit !== entry.sourceCommit) { + throw new Error('new public economic resilience capture differs from GitHub head SHA'); + } + const captureRoot = path.join(archiveRoot, 'source-tree', entry.sourceCommit); + for (const [file, expected] of [ + ['run-metadata.json', acquisition.runMetadataBytes], + ['artifact-metadata.json', acquisition.artifactMetadataBytes], + ]) { + const retained = readSecureFile( + path.join(captureRoot, file), + `new capture ${file}` + ); + if (!retained.equals(expected)) { + throw new Error(`new public economic resilience ${file} differs from GitHub`); + } + } + for (const platform of ['android', 'ios']) { + const retained = readSecureFile( + path.join(captureRoot, 'artifacts', `${platform}.zip`), + `new capture ${platform} artifact ZIP` + ); + const downloaded = readSecureFile( + acquisition.platforms[platform].zipFile, + `downloaded ${platform} artifact ZIP` + ); + if (!retained.equals(downloaded)) { + throw new Error( + `new public economic resilience ${platform} ZIP differs from GitHub artifact bytes` + ); + } + } + } finally { + acquisition.cleanup(); + } +} + +function readBaseEntries(base) { + const output = mustGitBytes([ + 'ls-tree', '-r', '-z', base, '--', PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT, + ], 'base archive tree'); + return output.toString('utf8').split('\0').filter(Boolean).map((record) => { + const match = /^(\d+)\s+(\S+)\s+([0-9a-f]+)\t(.+)$/u.exec(record); + if (!match || match[2] !== 'blob') { + throw new Error('base public economic resilience tree entry is invalid'); + } + const relativePath = path.posix.relative( + PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT, + match[4] + ); + if (!relativePath || relativePath.startsWith('../')) { + throw new Error('base public economic resilience path escaped its root'); + } + return { mode: match[1], path: match[4], relativePath }; + }); +} + +function readCurrentFiles(root, relativePaths) { + const files = new Map(); + for (const relative of relativePaths) { + const absolute = path.join(root, ...relative.split('/')); + if (existsSync(absolute)) { + files.set(relative, readSecureFile(absolute, `current archive ${relative}`)); + } + } + return files; +} + +function readSecureFile(file, label) { + const status = lstatSync(file); + if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1) { + throw new Error(`${label} must be a regular non-symlink file with exactly one hard link`); + } + return readFileSync(file); +} + +function mustGit(args, label) { + mustGitBytes(args, label); +} + +function mustGitBytes(args, label) { + const result = spawnSync('git', args, { + cwd: repositoryRoot, + encoding: null, + maxBuffer: 256 * 1024 * 1024, + timeout: 60_000, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`${label} cannot be read from Git: ${result.stderr.toString('utf8').trim()}`); + } + return result.stdout; +} + +function parseArgs(args) { + const values = args.filter((value) => value !== '--'); + if (values.length !== 2 || values[0] !== '--base' || !values[1]) { + throw new Error('usage: verify-public-economic-resilience-evolution --base '); + } + return { base: values[1] }; +} diff --git a/scripts/verify-site.mjs b/scripts/verify-site.mjs index 5eb80bc..a62c280 100644 --- a/scripts/verify-site.mjs +++ b/scripts/verify-site.mjs @@ -10,6 +10,10 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseHeadings, parseMarkdownLinks } from './docs-semantic-core.mjs'; import { inspectDemoEvidence } from './demo-evidence-core.mjs'; +import { + PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT, + inspectPublicEconomicResilienceArchive, +} from './public-economic-resilience-evidence-core.mjs'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const websiteRoot = path.join(root, 'website'); @@ -20,6 +24,8 @@ const requiredFiles = [ 'website/.vitepress/theme/index.ts', 'website/.vitepress/theme/OptionBuilder.vue', 'website/.vitepress/theme/ByteEconomicsCalculator.vue', + 'website/.vitepress/theme/EconomicResilienceArchive.vue', + 'website/.vitepress/economicArchiveData.ts', 'website/.vitepress/theme/byteEconomics.ts', 'website/.vitepress/theme/custom.css', 'website/public/logo.svg', @@ -40,6 +46,8 @@ const requiredFiles = [ 'website/reference/api.md', 'website/reference/architecture.md', 'website/reference/compatibility.md', + 'website/reference/evidence.md', + 'website/reference/economic-resilience.md', 'website/demo/index.md', 'website/roadmap.md', 'website/changelog.md', @@ -61,6 +69,7 @@ const siteConfig = readFileSync( for (const route of [ "link: '/reference/architecture'", "link: '/guide/choosing-an-image-pipeline'", + "link: '/reference/economic-resilience'", "link: '/roadmap'", ]) { if (!siteConfig.includes(route)) { @@ -137,6 +146,8 @@ const requiredSemanticText = [ 'capability-first', 'No roadmap item is a release promise', 'v0.4.0 iOS renderer', + 'matchedTransferBaseline: null', + 'costSavingsClaim: null', ]; if (releaseStatus.releaseState === 'candidate') { requiredSemanticText.push(`${packageJson.version} source candidate`); @@ -210,6 +221,39 @@ if (existsSync(demoManifestPath)) { errors.push('release state requires passed Android and iOS native demo evidence'); } +const publicEconomicArchive = inspectPublicEconomicResilienceArchive( + path.join(root, PUBLIC_ECONOMIC_RESILIENCE_ARCHIVE_ROOT) +); +if (publicEconomicArchive.status !== 'passed') { + errors.push(`public economic resilience archive: ${publicEconomicArchive.error}`); +} else { + const economicComponent = readFileSync( + path.join(websiteRoot, '.vitepress/theme/EconomicResilienceArchive.vue'), + 'utf8' + ); + const stateContracts = publicEconomicArchive.archiveState === 'available' + ? [ + 'Archived source-tree capture', + 'sourceToOutputByteDifference', + 'uprightSimilarity', + 'verticalFlipSimilarity', + 'removedPackageOutputs', + 'state.runUrl', + 'Original artifact ZIP', + ] + : [ + 'Methodology preview · no archived capture', + 'not a package release or a measured product result', + ]; + for (const contract of stateContracts) { + if (!economicComponent.includes(contract)) { + errors.push( + `economic resilience ${publicEconomicArchive.archiveState} component missing: ${contract}` + ); + } + } +} + if (errors.length > 0) { console.error(`Site verification failed:\n- ${errors.join('\n- ')}`); process.exit(1); diff --git a/test/docsSemantic.test.mjs b/test/docsSemantic.test.mjs index ae5551c..ec1bc90 100644 --- a/test/docsSemantic.test.mjs +++ b/test/docsSemantic.test.mjs @@ -6,6 +6,8 @@ import { describe, expect, it } from 'vitest'; import { collectMarkdownLinkViolations, inspectDocumentation, + inspectEconomicResiliencePageContracts, + inspectEconomicResilienceStateContracts, inspectProductEvidenceContracts, inspectProductDirectionContracts, inspectReleaseLanguageContracts, @@ -177,6 +179,39 @@ describe('documentation semantic gate', () => { ]); }); + it('keeps source-tree economic evidence append-only and inside measured claim boundaries', () => { + const evidence = readFileSync( + path.join(ROOT, 'website/reference/economic-resilience.md'), + 'utf8' + ); + + expect(inspectEconomicResiliencePageContracts(evidence)).toEqual([]); + const component = readFileSync( + path.join(ROOT, 'website/.vitepress/theme/EconomicResilienceArchive.vue'), + 'utf8' + ); + expect(inspectEconomicResilienceStateContracts({ + archiveState: 'empty', + componentContents: component, + })).toEqual([]); + expect(inspectEconomicResilienceStateContracts({ + archiveState: 'available', + componentContents: component, + })).toEqual([]); + expect( + inspectEconomicResiliencePageContracts( + evidence + .replace('## Economic claim boundary', '## Savings') + .replace('matchedTransferBaseline: null', 'baseline measured') + .replace('There is no mutable `latest`', 'Use the latest alias') + ) + ).toEqual([ + 'economic resilience evidence missing heading: Economic claim boundary', + 'economic resilience evidence missing decision contract: matchedTransferBaseline: null', + 'economic resilience evidence missing decision contract: There is no mutable `latest`', + ]); + }); + it('parses only marked README and RELEASE status blocks', () => { const readme = [ 'Historical Status: v1.2.1 candidate', diff --git a/test/economicResilienceArchiveData.test.ts b/test/economicResilienceArchiveData.test.ts new file mode 100644 index 0000000..2e706f5 --- /dev/null +++ b/test/economicResilienceArchiveData.test.ts @@ -0,0 +1,118 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { loadEconomicArchiveSnapshot } from '../website/.vitepress/economicArchiveData'; + +const roots: string[] = []; +const SHA = 'a'.repeat(40); + +afterEach(() => { + while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true }); +}); + +describe('economic resilience page archive data', () => { + it('renders the honest empty methodology state without an archive', () => { + expect(loadEconomicArchiveSnapshot(temporaryWebsite())).toEqual({ kind: 'empty' }); + }); + + it('derives every displayed result and asset identity from the latest index entry', () => { + const website = temporaryWebsite(); + const archive = path.join(website, 'public/evidence/economic-resilience'); + const root = path.join(archive, 'source-tree', SHA); + mkdirSync(path.join(root, 'artifacts'), { recursive: true }); + writeJson(path.join(archive, 'index.json'), { + captures: [{ + sourceCommit: SHA, + runId: 42, + captureSetPath: `source-tree/${SHA}/capture-set.json`, + }], + }); + writeJson(path.join(root, 'capture-set.json'), { + sourceCommit: SHA, + workflow: { + runId: 42, + runAttempt: 3, + runUrl: + 'https://github.com/GGULBAE/react-native-image-compression-kit/actions/runs/42', + }, + }); + for (const [platform, offset] of [['android', 0], ['ios', 1]] as const) { + const platformRoot = path.join(root, platform); + mkdirSync(platformRoot); + writeJson(path.join(platformRoot, 'economic-resilience.json'), { + economics: { + sourceBytes: 1_721_333, + outputBytes: 400_000 + offset, + sourceToOutputByteDifference: 1_321_333 - offset, + }, + cleanup: { + attemptedPackageOutputs: 12, + removedPackageOutputs: 12, + residualPackageOutputBytes: 0, + }, + }); + writeJson(path.join(platformRoot, 'visual-agreement.json'), { + uprightSimilarity: 0.98 - offset * 0.01, + verticalFlipSimilarity: 0.5, + }); + writeFileSync(path.join(platformRoot, 'source.jpg'), 'source'); + writeFileSync(path.join(platformRoot, 'output.jpg'), 'output'); + writeFileSync(path.join(root, 'artifacts', `${platform}.zip`), 'zip'); + } + + expect(loadEconomicArchiveSnapshot(website)).toEqual({ + kind: 'available', + count: 1, + sourceCommit: SHA, + runId: 42, + runAttempt: 3, + runUrl: + 'https://github.com/GGULBAE/react-native-image-compression-kit/actions/runs/42', + platforms: [ + { + platform: 'android', + root: `source-tree/${SHA}`, + sourceBytes: 1_721_333, + outputBytes: 400_000, + sourceToOutputByteDifference: 1_321_333, + uprightSimilarity: 0.98, + verticalFlipSimilarity: 0.5, + attemptedPackageOutputs: 12, + removedPackageOutputs: 12, + residualPackageOutputBytes: 0, + }, + expect.objectContaining({ + platform: 'ios', + outputBytes: 400_001, + sourceToOutputByteDifference: 1_321_332, + uprightSimilarity: 0.97, + }), + ], + }); + }); + + it('fails closed instead of presenting a partial available snapshot', () => { + const website = temporaryWebsite(); + const archive = path.join(website, 'public/evidence/economic-resilience'); + mkdirSync(archive, { recursive: true }); + writeJson(path.join(archive, 'index.json'), { + captures: [{ + sourceCommit: SHA, + runId: 42, + captureSetPath: `source-tree/${SHA}/capture-set.json`, + }], + }); + expect(() => loadEconomicArchiveSnapshot(website)).toThrow(); + }); +}); + +function temporaryWebsite() { + const root = mkdtempSync(path.join(os.tmpdir(), 'rnick-economic-page-')); + roots.push(root); + return root; +} + +function writeJson(file: string, value: unknown) { + writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} diff --git a/test/fixtures/public-economic-resilience/github-run-pr-failure.json b/test/fixtures/public-economic-resilience/github-run-pr-failure.json new file mode 100644 index 0000000..e89e383 --- /dev/null +++ b/test/fixtures/public-economic-resilience/github-run-pr-failure.json @@ -0,0 +1,11 @@ +{ + "event": "pull_request", + "headBranch": "codex/add-12mp-economic-evidence", + "headSha": "5666a6beb0cdf499b442a942535e5fe0d03c9fc7", + "conclusion": "failure", + "workflowName": "Native Demo Evidence", + "path": ".github/workflows/demo-evidence.yml", + "runId": 31690941347, + "attempt": 1, + "url": "https://github.com/GGULBAE/react-native-image-compression-kit/actions/runs/31690941347" +} diff --git a/test/publicEconomicResilienceEvidence.test.mjs b/test/publicEconomicResilienceEvidence.test.mjs new file mode 100644 index 0000000..30ea5a8 --- /dev/null +++ b/test/publicEconomicResilienceEvidence.test.mjs @@ -0,0 +1,748 @@ +import { createHash } from 'node:crypto'; +import { spawnSync } from 'node:child_process'; +import { + chmodSync, + cpSync, + linkSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + renameSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { + ECONOMIC_RESILIENCE_ASSET_FILES, + ECONOMIC_RESILIENCE_FIXTURE, + ECONOMIC_RESILIENCE_OPERATION, + buildEconomicResilienceEvidence, +} from '../scripts/economic-resilience-evidence-core.mjs'; +import { PORTABLE_DEMO_VISUAL_AGREEMENT_PROFILE } from '../scripts/demo-visual-agreement-core.mjs'; +import { + PUBLIC_ECONOMIC_RESILIENCE_ARTIFACT_METADATA_FILE, + PUBLIC_ECONOMIC_RESILIENCE_CAPTURE_SET_FILE, + PUBLIC_ECONOMIC_RESILIENCE_RUN_METADATA_FILE, + appendPublicEconomicResilienceIndex, + buildPublicEconomicResilienceCaptureSet, + createEmptyPublicEconomicResilienceIndex, + inspectPublicEconomicResilienceArchive, + inspectPublicEconomicResilienceArtifactMetadata, + inspectPublicEconomicResilienceCaptureSet, + inspectPublicEconomicResilienceIndex, + inspectPublicEconomicResilienceRunMetadata, +} from '../scripts/public-economic-resilience-evidence-core.mjs'; +import { PUBLIC_ECONOMIC_RESILIENCE_NATIVE_ARTIFACT_MEMBERS } from '../scripts/public-economic-resilience-zip.mjs'; + +const SHA = '1'.repeat(40); +const RUN_ID = 424_242_424; +const RUN_ATTEMPT = 1; +const RUN_URL = + `https://github.com/GGULBAE/react-native-image-compression-kit/actions/runs/${RUN_ID}`; +const SOURCE = readFileSync('example/fixtures/kit-only-12mp-v1.jpg'); +const FIXTURE_MANIFEST = JSON.parse( + readFileSync('example/fixtures/kit-only-12mp-v1.json', 'utf8') +); +const PR_RUN_METADATA_PATH = path.resolve( + 'test/fixtures/public-economic-resilience/github-run-pr-failure.json' +); +const PR_RUN_METADATA = JSON.parse(readFileSync(PR_RUN_METADATA_PATH, 'utf8')); +const IMPORTER = path.resolve('scripts/import-public-economic-resilience-evidence.mjs'); +const ARCHIVE_VERIFIER = path.resolve( + 'scripts/verify-public-economic-resilience-evidence.mjs' +); +const RUN_METADATA = Object.freeze({ + event: 'workflow_dispatch', + headBranch: 'master', + headSha: SHA, + conclusion: 'success', + workflowName: 'Native Demo Evidence', + path: '.github/workflows/demo-evidence.yml', + runId: RUN_ID, + attempt: RUN_ATTEMPT, + url: RUN_URL, +}); +const RUN_METADATA_BYTES = serializeJson(RUN_METADATA); +const roots = []; +let githubFixture; + +beforeAll(() => { + githubFixture = createGithubFixture(); +}, 60_000); + +afterAll(() => { + while (roots.length > 0) rmSync(roots.pop(), { recursive: true, force: true }); +}); + +describe('public economic resilience source-tree archive', () => { + it('accepts an absent archive or a schema-valid empty index as methodology-only state', () => { + const parent = temporaryRoot(); + expect( + inspectPublicEconomicResilienceArchive(path.join(parent, 'absent')) + ).toMatchObject({ + status: 'passed', + archiveState: 'empty', + captureCount: 0, + }); + const empty = path.join(parent, 'empty'); + mkdirSync(empty); + writeJson(path.join(empty, 'index.json'), createEmptyPublicEconomicResilienceIndex()); + expect(inspectPublicEconomicResilienceArchive(empty)).toMatchObject({ + status: 'passed', + archiveState: 'empty', + captureCount: 0, + }); + }); + + it('binds exact run and artifact provenance while preserving the claim boundary', () => { + const android = validEvidenceBoundary('android'); + const ios = validEvidenceBoundary('ios'); + const artifactMetadata = syntheticArtifactMetadata(); + const artifactMetadataBytes = serializeJson(artifactMetadata); + const captureSet = buildPublicEconomicResilienceCaptureSet({ + sourceCommit: SHA, + runMetadata: RUN_METADATA, + runMetadataBytes: RUN_METADATA_BYTES, + artifactMetadata, + artifactMetadataBytes, + androidEvidence: android, + iosEvidence: ios, + }); + const index = appendPublicEconomicResilienceIndex( + createEmptyPublicEconomicResilienceIndex(), + captureSet + ); + expect(inspectPublicEconomicResilienceCaptureSet(captureSet, { + androidEvidence: android, + iosEvidence: ios, + runMetadata: RUN_METADATA, + runMetadataBytes: RUN_METADATA_BYTES, + artifactMetadata, + artifactMetadataBytes, + })).toEqual([]); + expect(inspectPublicEconomicResilienceIndex(index)).toEqual([]); + expect(captureSet).toMatchObject({ + sourceCommit: SHA, + artifactMetadata: { file: 'artifact-metadata.json' }, + workflow: { + eventName: 'workflow_dispatch', + headBranch: 'master', + headSha: SHA, + runId: RUN_ID, + }, + claimBoundary: { + sourceOwnership: 'source-remains', + matchedTransferBaseline: null, + costSavingsClaim: null, + }, + platforms: { + android: { artifactArchivePath: 'artifacts/android.zip' }, + ios: { artifactArchivePath: 'artifacts/ios.zip' }, + }, + }); + expect(() => appendPublicEconomicResilienceIndex(index, captureSet)).toThrow( + `already exists for ${SHA}` + ); + }); + + it('fails closed across malformed run, artifact, index, and cross-platform inputs', () => { + const base = { + sourceCommit: SHA, + runMetadata: RUN_METADATA, + runMetadataBytes: RUN_METADATA_BYTES, + artifactMetadata: syntheticArtifactMetadata(), + artifactMetadataBytes: serializeJson(syntheticArtifactMetadata()), + androidEvidence: validEvidenceBoundary('android'), + iosEvidence: validEvidenceBoundary('ios'), + }; + expect(() => buildPublicEconomicResilienceCaptureSet({ + ...base, + runMetadata: { ...RUN_METADATA, event: 'pull_request' }, + })).toThrow('successful master workflow_dispatch run'); + expect(() => buildPublicEconomicResilienceCaptureSet({ + ...base, + runMetadataBytes: null, + })).toThrow('run metadata bytes are required'); + expect(() => buildPublicEconomicResilienceCaptureSet({ + ...base, + artifactMetadata: {}, + })).toThrow('artifact metadata'); + expect(() => buildPublicEconomicResilienceCaptureSet({ + ...base, + artifactMetadataBytes: null, + })).toThrow('artifact metadata bytes are required'); + expect(() => buildPublicEconomicResilienceCaptureSet({ + ...base, + iosEvidence: { ...base.iosEvidence, runAttempt: 2 }, + })).toThrow('ios run identity differs'); + + expect(inspectPublicEconomicResilienceRunMetadata({ + ...RUN_METADATA, + extra: true, + })).toContain('public economic resilience run metadata fields drifted'); + expect(inspectPublicEconomicResilienceArtifactMetadata({}, { + runMetadata: RUN_METADATA, + })).toEqual([ + 'public economic resilience artifact metadata fields drifted', + 'public economic resilience artifact metadata run identity is invalid', + 'public economic resilience artifact metadata must contain two artifacts', + ]); + const malformedArtifacts = syntheticArtifactMetadata(); + malformedArtifacts.artifacts[0].extra = true; + malformedArtifacts.artifacts[1].expired = true; + expect(inspectPublicEconomicResilienceArtifactMetadata(malformedArtifacts, { + runMetadata: RUN_METADATA, + }).join(' | ')).toContain('artifact fields drifted'); + expect(inspectPublicEconomicResilienceArtifactMetadata(malformedArtifacts, { + runMetadata: RUN_METADATA, + }).join(' | ')).toContain('artifact identity is invalid'); + expect(inspectPublicEconomicResilienceIndex({})).toEqual([ + 'public economic resilience index fields drifted', + 'public economic resilience index schemaVersion is invalid', + 'public economic resilience archive identity drifted', + 'public economic resilience index captures must be an array', + ]); + const validCapture = buildPublicEconomicResilienceCaptureSet(base); + expect(() => appendPublicEconomicResilienceIndex({}, validCapture)).toThrow( + 'index fields drifted' + ); + }); + + it('rejects a captured pull-request run before accepting any artifacts', () => { + expect( + inspectPublicEconomicResilienceRunMetadata(PR_RUN_METADATA, { + sourceCommit: PR_RUN_METADATA.headSha, + }) + ).toEqual([ + 'public economic resilience run metadata must identify a successful master workflow_dispatch run', + ]); + const archive = path.join(temporaryRoot(), 'archive'); + const result = runImporter(archive, { FAKE_GH_MODE: 'pr' }, PR_RUN_METADATA.runId); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain('successful master workflow_dispatch run'); + expect(readdirSync(path.dirname(archive))).not.toContain(path.basename(archive)); + expect(transactionResidue(path.dirname(archive))).toEqual([]); + }); + + it('imports real replayable JPEG bundles through the gh-backed CLI and verifies retained ZIPs', () => { + const parent = temporaryRoot(); + const archive = path.join(parent, 'archive'); + const imported = runImporter(archive); + expect(imported.status, imported.stderr).toBe(0); + expect(JSON.parse(imported.stdout)).toMatchObject({ + status: 'passed', + archiveState: 'available', + captureCount: 1, + importedSourceCommit: SHA, + }); + const report = spawnSync( + process.execPath, + [ARCHIVE_VERIFIER, '--archive-root', archive], + { encoding: 'utf8', timeout: 120_000 } + ); + expect(report.status, report.stderr).toBe(0); + expect(JSON.parse(report.stdout)).toMatchObject({ + status: 'passed', + captureCount: 1, + replay: [ + { sourceCommit: SHA, platform: 'android', status: 'passed' }, + { sourceCommit: SHA, platform: 'ios', status: 'passed' }, + ], + }); + const capture = path.join(archive, 'source-tree', SHA); + expect(readdirSync(capture).sort()).toEqual([ + PUBLIC_ECONOMIC_RESILIENCE_ARTIFACT_METADATA_FILE, + PUBLIC_ECONOMIC_RESILIENCE_CAPTURE_SET_FILE, + 'android', + 'artifacts', + 'ios', + PUBLIC_ECONOMIC_RESILIENCE_RUN_METADATA_FILE, + ].sort()); + expect(readFileSync(path.join(capture, 'artifacts', 'android.zip'))).toEqual( + readFileSync(githubFixture.zips.android) + ); + expect(readFileSync(path.join(capture, 'artifacts', 'ios.zip'))).toEqual( + readFileSync(githubFixture.zips.ios) + ); + expect(transactionResidue(parent)).toEqual([]); + + const before = snapshotTree(archive); + const duplicate = runImporter(archive); + expect(duplicate.status).not.toBe(0); + expect(duplicate.stderr).toContain(`already exists for ${SHA}`); + expect(snapshotTree(archive)).toEqual(before); + expect(transactionResidue(parent)).toEqual([]); + }, 120_000); + + it('rejects hard links, retained ZIP drift, orphan entries, and SHA traversal', () => { + const baseline = importedArchive(); + for (const [expected, mutate] of [ + [ + 'exactly one hard link', + (archive) => linkSync( + path.join(archive, 'index.json'), + path.join(path.dirname(archive), 'index-hardlink.json') + ), + ], + [ + 'exactly one hard link', + (archive) => linkSync( + path.join(archive, 'source-tree', SHA, 'android', 'source.jpg'), + path.join(path.dirname(archive), 'source-hardlink.jpg') + ), + ], + [ + 'retained android artifact ZIP binding differs', + (archive) => writeFileSync( + path.join(archive, 'source-tree', SHA, 'artifacts', 'android.zip'), + Buffer.from('changed') + ), + ], + [ + 'orphan entries', + (archive) => renameSync( + path.join(archive, 'source-tree', SHA), + path.join(archive, 'source-tree', 'b'.repeat(40)) + ), + ], + [ + 'index capture 1 SHA is invalid', + (archive) => { + const indexPath = path.join(archive, 'index.json'); + const index = readJson(indexPath); + index.captures[0].sourceCommit = '../../outside'; + index.captures[0].captureSetPath = + 'source-tree/../../outside/capture-set.json'; + writeJson(indexPath, index); + }, + ], + ]) { + const parent = temporaryRoot(); + const archive = path.join(parent, 'archive'); + cpSync(baseline, archive, { recursive: true }); + mutate(archive); + const report = inspectPublicEconomicResilienceArchive(archive); + expect(report.status).toBe('failed'); + expect(report.error).toContain(expected); + } + }, 120_000); + + it.each([ + ['before-journal', true], + ['after-journal', true], + ['after-capture-rename', false], + ['after-index-publish', false], + ])('recovers crash failpoint %s without transaction residue', (failpoint, retrySucceeds) => { + const parent = temporaryRoot(); + const archive = path.join(parent, 'archive'); + const crashed = runImporter(archive, { + NODE_ENV: 'test', + RNICK_PUBLIC_EVIDENCE_FAILPOINT: failpoint, + }); + expect(crashed.signal).toBe('SIGKILL'); + const recovered = runImporter(archive); + if (retrySucceeds) expect(recovered.status, recovered.stderr).toBe(0); + else { + expect(recovered.status).not.toBe(0); + expect(recovered.stderr).toContain(`already exists for ${SHA}`); + } + expect(inspectPublicEconomicResilienceArchive(archive)).toMatchObject({ + status: 'passed', + archiveState: 'available', + captureCount: 1, + }); + expect(transactionResidue(parent)).toEqual([]); + }, 120_000); + + it('keeps CLI acquisition narrow and public copy project-generic', () => { + const importer = readFileSync(IMPORTER, 'utf8'); + expect(importer).toContain('acquirePublicEconomicResilienceRun(runId)'); + expect(importer).toContain("renameSync(stagedCapture, destinationCapture)"); + expect(importer).toContain("triggerFailpoint('after-capture-rename')"); + expect(importer).not.toContain('--android-artifact'); + expect(importer).not.toContain('--run-metadata'); + const page = readFileSync('website/reference/economic-resilience.md', 'utf8'); + expect(page).not.toMatch(/fastest|universally faster|money saved/i); + expect(page).toContain('matchedTransferBaseline: null'); + expect(page).toContain('costSavingsClaim: null'); + }); +}); + +function createGithubFixture() { + const parent = temporaryRoot(); + const generatedOutput = path.join(parent, 'output.jpg'); + mustRun('ffmpeg', [ + '-hide_banner', '-loglevel', 'error', + '-i', path.resolve('example/fixtures/kit-only-12mp-v1.jpg'), + '-vf', 'scale=1600:1200:flags=lanczos', + '-frames:v', '1', '-c:v', 'mjpeg', '-q:v', '2', + '-pix_fmt', 'yuvj420p', '-map_metadata', '-1', + '-flags', '+bitexact', '-fflags', '+bitexact', generatedOutput, + ]); + const visualPath = path.join(parent, 'visual.json'); + mustRun(process.execPath, [ + path.resolve('scripts/measure-demo-visual-agreement.mjs'), + '--source', path.resolve('example/fixtures/kit-only-12mp-v1.jpg'), + '--output', generatedOutput, + '--resize-mode', 'contain', '--max-width', '1600', '--max-height', '1200', + '--comparison-profile', PORTABLE_DEMO_VISUAL_AGREEMENT_PROFILE, + '--report', visualPath, + ]); + const outputBytes = readFileSync(generatedOutput); + const visualAgreement = readJson(visualPath); + const artifacts = []; + const zips = {}; + for (const [platform, id] of [['android', 101], ['ios', 102]]) { + const nativeRoot = path.join(parent, `native-${platform}`); + const economicRoot = path.join(nativeRoot, 'economic-resilience'); + mkdirSync(economicRoot, { recursive: true }); + writeReplayableEvidence({ + root: economicRoot, + platform, + outputBytes, + visualAgreement, + }); + for (const member of PUBLIC_ECONOMIC_RESILIENCE_NATIVE_ARTIFACT_MEMBERS) { + const file = path.join(nativeRoot, member); + if (member.startsWith('economic-resilience/')) continue; + if (member === 'source.jpg') writeFileSync(file, SOURCE); + else if (member === 'output.jpg' || member === 'screen.png') { + writeFileSync(file, outputBytes); + } else writeFileSync(file, `${platform}-${member}\n`); + } + const zipFile = path.join(parent, `${platform}.zip`); + mustRun('zip', [ + '-q', '-X', zipFile, + ...PUBLIC_ECONOMIC_RESILIENCE_NATIVE_ARTIFACT_MEMBERS, + ], { cwd: nativeRoot }); + zips[platform] = zipFile; + const zipBytes = readFileSync(zipFile); + artifacts.push({ + id, + name: `native-demo-${platform}-${SHA}`, + digest: `sha256:${sha256(zipBytes)}`, + expired: false, + size_in_bytes: zipBytes.length, + archive_download_url: + `https://api.github.com/repos/GGULBAE/react-native-image-compression-kit/actions/artifacts/${id}/zip`, + workflow_run: { id: RUN_ID, head_branch: 'master', head_sha: SHA }, + }); + } + const config = { + run: { + event: RUN_METADATA.event, + head_branch: RUN_METADATA.headBranch, + head_sha: RUN_METADATA.headSha, + conclusion: RUN_METADATA.conclusion, + name: RUN_METADATA.workflowName, + path: RUN_METADATA.path, + id: RUN_METADATA.runId, + run_attempt: RUN_METADATA.attempt, + html_url: RUN_METADATA.url, + }, + prRun: { + event: PR_RUN_METADATA.event, + head_branch: PR_RUN_METADATA.headBranch, + head_sha: PR_RUN_METADATA.headSha, + conclusion: PR_RUN_METADATA.conclusion, + name: PR_RUN_METADATA.workflowName, + path: PR_RUN_METADATA.path, + id: PR_RUN_METADATA.runId, + run_attempt: PR_RUN_METADATA.attempt, + html_url: PR_RUN_METADATA.url, + }, + artifacts, + zips, + }; + const configPath = path.join(parent, 'github.json'); + writeJson(configPath, config); + const bin = path.join(parent, 'bin'); + mkdirSync(bin); + const gh = path.join(bin, 'gh'); + writeFileSync(gh, `#!/usr/bin/env node +const fs = require('node:fs'); +const config = JSON.parse(fs.readFileSync(process.env.FAKE_GH_CONFIG, 'utf8')); +const endpoint = process.argv.at(-1); +if (process.env.FAKE_GH_MODE === 'pr') { + process.stdout.write(JSON.stringify(config.prRun)); +} else if (/actions\\/runs\\/\\d+\\/artifacts/.test(endpoint)) { + process.stdout.write(JSON.stringify({total_count: 2, artifacts: config.artifacts})); +} else if (/actions\\/runs\\/\\d+$/.test(endpoint)) { + process.stdout.write(JSON.stringify(config.run)); +} else { + const artifact = config.artifacts.find((item) => endpoint.includes('/artifacts/' + item.id + '/zip')); + if (!artifact) process.exit(2); + fs.writeFileSync(1, fs.readFileSync(config.zips[artifact.name.includes('-android-') ? 'android' : 'ios'])); +} +`); + chmodSync(gh, 0o755); + return { configPath, bin, zips }; +} + +function writeReplayableEvidence({ root, platform, outputBytes, visualAgreement }) { + const payload = validPayload(platform, outputBytes); + const environment = validEnvironment(platform); + environment.toolchain.ffmpeg = firstLine( + mustRun('ffmpeg', ['-version'], { encoding: 'utf8' }).stdout + ); + environment.toolchain.ffprobe = firstLine( + mustRun('ffprobe', ['-version'], { encoding: 'utf8' }).stdout + ); + const evidence = buildEconomicResilienceEvidence({ + payload, + packageVersion: '0.4.1', + sourceCommit: SHA, + runId: RUN_ID, + runAttempt: RUN_ATTEMPT, + capturedAt: '2026-08-13T07:00:00.000Z', + runUrl: RUN_URL, + environment, + fixtureManifest: FIXTURE_MANIFEST, + sourceBytes: SOURCE, + outputBytes, + visualAgreement, + }); + writeFileSync(path.join(root, 'source.jpg'), SOURCE); + writeFileSync(path.join(root, 'output.jpg'), outputBytes); + writeJson(path.join(root, 'fixture-manifest.json'), FIXTURE_MANIFEST); + writeJson(path.join(root, 'visual-agreement.json'), visualAgreement); + writeJson(path.join(root, 'environment.json'), environment); + writeJson(path.join(root, 'economic-resilience.json'), evidence); + expect(readdirSync(root).sort()).toEqual(ECONOMIC_RESILIENCE_ASSET_FILES); +} + +function validPayload(platform, outputBytes) { + const inspection = { + exists: true, + byteSize: outputBytes.length, + sha256: sha256(outputBytes), + mediaType: 'image/jpeg', + width: 1600, + height: 1200, + }; + const samples = Array.from({ length: 12 }, (_, index) => ({ + phase: index < 2 ? 'warmup' : 'measured', + iteration: index < 2 ? index + 1 : index - 1, + elapsedMs: index + 1, + result: { + format: 'jpeg', width: 1600, height: 1200, + byteSize: outputBytes.length, + originalByteSize: SOURCE.length, + compressionRatio: outputBytes.length / SOURCE.length, + }, + sourceToOutputByteDifference: SOURCE.length - outputBytes.length, + outputInspection: { ...inspection }, + cleanup: { + packageOutputRemoved: true, + existsAfterRemoval: false, + residualByteSize: 0, + }, + })); + return { + schemaVersion: 1, + scenarioId: 'kit-only-12mp-jpeg-v1', + implementation: { name: 'react-native-image-compression-kit' }, + platform, + architecture: 'new', + jsEngine: 'hermes', + fixture: { + ...ECONOMIC_RESILIENCE_FIXTURE, + sourceUri: 'file:///cache/source.jpg', + inspection: { + exists: true, byteSize: SOURCE.length, sha256: sha256(SOURCE), + mediaType: 'image/jpeg', width: 4000, height: 3000, + }, + remainsAfterRun: true, + }, + operation: ECONOMIC_RESILIENCE_OPERATION, + capabilities: validCapabilities(platform), + timing: { + clock: 'performance.now', boundary: 'compressImage-call-only', + warmupIterations: 2, measuredIterations: 10, + }, + representative: { + measuredIteration: 10, + stagedOutputUri: 'file:///cache/staged-output.jpg', + inspection: { ...inspection }, + }, + samples, + cleanup: { + attemptedPackageOutputs: 12, removedPackageOutputs: 12, + residualPackageOutputs: 0, residualPackageOutputBytes: 0, + }, + }; +} + +function validCapabilities(platform) { + return { + platform, + formats: ['jpeg', 'png', 'webp', 'heic', 'heif', 'avif', 'gif'].map( + (format) => ({ + format, + input: true, + output: ['jpeg', 'png', 'webp'].includes(format), + supportsAlpha: format !== 'jpeg', + supportsAnimation: false, + notes: [`${format} exact runtime note`], + }) + ), + metadataPolicies: ['preserve', 'safe', 'strip'], + supportsTargetSizeCompression: true, + supportsCancellation: true, + maxConcurrentOperations: 2, + supportsDecodeDownsampling: true, + resourceLimits: { + maxSourceDimension: 16_384, + maxSourcePixels: 48_000_000, + maxWorkingPixels: 16_000_000, + }, + }; +} + +function validEnvironment(platform) { + return { + platform, + runtime: platform === 'android' ? 'Android 15 / API 35' : 'iOS 18.0', + osBuild: 'exact-build', + device: platform === 'android' ? 'Google emulator' : 'iPhone simulator', + deviceKind: platform === 'android' ? 'emulator' : 'simulator', + abi: platform === 'android' ? 'x86_64' : 'arm64', + reactNativeArchitecture: 'new', + reactNativeVersion: '0.86.2', + jsEngine: 'hermes', + buildType: 'debug', + runner: { + label: platform === 'android' ? 'ubuntu-latest' : 'macos-latest', + os: platform === 'android' ? 'Linux' : 'macOS', + arch: 'ARM64', + name: 'GitHub Actions exact runner', + imageOS: platform === 'android' ? 'ubuntu24' : 'macos15', + imageVersion: '20260810.1', + }, + toolchain: { + node: 'v24.18.0', ffmpeg: 'pending', ffprobe: 'pending', + primary: platform === 'android' ? 'openjdk 21.0.8' : 'Xcode 16.0', + platformSdk: platform === 'android' ? 'Android SDK 35' : 'iOS SDK 18.0', + }, + }; +} + +function syntheticArtifactMetadata() { + return { + schemaVersion: 1, + runId: RUN_ID, + artifacts: ['android', 'ios'].map((platform, index) => ({ + platform, + id: index + 1, + name: `native-demo-${platform}-${SHA}`, + digest: `sha256:${String(index + 1).repeat(64)}`, + expired: false, + sizeInBytes: 100 + index, + archiveDownloadUrl: + `https://api.github.com/repos/GGULBAE/react-native-image-compression-kit/actions/artifacts/${index + 1}/zip`, + })), + }; +} + +function validEvidenceBoundary(platform) { + return { + implementation: { version: '0.4.1', buildSource: 'checked-out-source-tree' }, + sourceCommit: SHA, + runId: RUN_ID, + runAttempt: RUN_ATTEMPT, + runUrl: RUN_URL, + environment: { platform }, + fixture: { remainsAfterRun: true, sha256: sha256(SOURCE), byteSize: SOURCE.length }, + economics: { + boundary: 'source-to-output-observation', sourceOwnership: 'source-remains', + matchedTransferBaseline: null, costSavingsClaim: null, + }, + cleanup: { + attemptedPackageOutputs: 12, removedPackageOutputs: 12, + residualPackageOutputs: 0, residualPackageOutputBytes: 0, + }, + visualAgreement: { + schemaVersion: 3, + comparisonProfile: PORTABLE_DEMO_VISUAL_AGREEMENT_PROFILE, + }, + scenarioId: 'kit-only-12mp-jpeg-v1', + }; +} + +function importedArchive() { + const archive = path.join(temporaryRoot(), 'archive'); + const result = runImporter(archive); + expect(result.status, result.stderr).toBe(0); + return archive; +} + +function runImporter(archive, environment = {}, runId = RUN_ID) { + return spawnSync( + process.execPath, + [IMPORTER, '--run-id', String(runId), '--archive-root', archive], + { + encoding: 'utf8', + timeout: 120_000, + env: { + ...process.env, + PATH: `${githubFixture.bin}${path.delimiter}${process.env.PATH}`, + FAKE_GH_CONFIG: githubFixture.configPath, + ...environment, + }, + } + ); +} + +function transactionResidue(parent) { + return readdirSync(parent).filter((entry) => entry.startsWith('.economic-resilience')); +} + +function snapshotTree(root) { + const snapshot = {}; + const visit = (directory, prefix = '') => { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const relative = path.posix.join(prefix, entry.name); + const absolute = path.join(directory, entry.name); + if (entry.isDirectory()) visit(absolute, relative); + else snapshot[relative] = sha256(readFileSync(absolute)); + } + }; + visit(root); + return snapshot; +} + +function temporaryRoot() { + const root = mkdtempSync(path.join(os.tmpdir(), 'rnick-public-economic-')); + roots.push(root); + return root; +} + +function readJson(file) { + return JSON.parse(readFileSync(file, 'utf8')); +} + +function writeJson(file, value) { + writeFileSync(file, serializeJson(value)); +} + +function serializeJson(value) { + return Buffer.from(`${JSON.stringify(value, null, 2)}\n`); +} + +function mustRun(command, args, options = {}) { + const result = spawnSync(command, args, { encoding: 'utf8', ...options }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error(result.stderr || result.stdout); + return result; +} + +function firstLine(value) { + return String(value).split(/\r?\n/u)[0]; +} + +function sha256(value) { + return createHash('sha256').update(value).digest('hex'); +} diff --git a/test/publicEconomicResilienceEvolution.test.mjs b/test/publicEconomicResilienceEvolution.test.mjs new file mode 100644 index 0000000..83d4c3a --- /dev/null +++ b/test/publicEconomicResilienceEvolution.test.mjs @@ -0,0 +1,110 @@ +import { describe, expect, it } from 'vitest'; +import { inspectPublicEconomicResilienceEvolution } from '../scripts/public-economic-resilience-evolution-core.mjs'; + +const A = 'a'.repeat(40); +const B = 'b'.repeat(40); +const capture = (sha, runId) => ({ + sourceCommit: sha, + runId, + captureSetPath: `source-tree/${sha}/capture-set.json`, +}); +const index = (captures) => ({ + schemaVersion: 1, + archive: 'economic-resilience-source-tree-v1', + captures, +}); +const baseIndex = index([capture(A, 1)]); +const baseFiles = new Map([ + ['index.json', Buffer.from('base-index')], + [`source-tree/${A}/capture-set.json`, Buffer.from('capture')], + [`source-tree/${A}/artifacts/android.zip`, Buffer.from('zip')], +]); + +describe('public economic resilience revision evolution', () => { + it('accepts only an exact historical prefix plus a full-SHA suffix', () => { + expect(check({ + currentIndex: index([capture(A, 1), capture(B, 2)]), + currentFiles: new Map([...baseFiles, [ + `source-tree/${B}/capture-set.json`, Buffer.from('new'), + ]]), + })).toEqual([]); + }); + + it.each([ + ['deleted archive', { + currentArchivePresent: false, + currentIndex: index([]), + currentFiles: new Map(), + }, 'archive was removed'], + ['subset rewrite', { + currentIndex: index([]), + currentFiles: new Map(baseFiles), + }, 'history was truncated'], + ['reordered captures', { + currentIndex: index([capture(B, 2), capture(A, 1)]), + currentFiles: new Map(baseFiles), + }, 'not an exact ordered prefix'], + ['modified bytes', { + currentIndex: baseIndex, + currentFiles: new Map([...baseFiles].map(([name, bytes]) => [ + name, + name.endsWith('android.zip') ? Buffer.from('changed') : bytes, + ])), + }, 'file changed'], + ['modified metadata', { + currentIndex: baseIndex, + currentFiles: new Map([...baseFiles].map(([name, bytes]) => [ + name, + name.endsWith('capture-set.json') ? Buffer.from('changed') : bytes, + ])), + }, 'file changed'], + ['removed retained file', { + currentIndex: baseIndex, + currentFiles: new Map([['index.json', Buffer.from('current')]]), + }, 'file was removed'], + ])('rejects %s', (_name, overrides, expected) => { + expect(check(overrides).join(' | ')).toContain(expected); + }); + + it('treats a missing base archive as first publication', () => { + expect(inspectPublicEconomicResilienceEvolution({ + baseArchivePresent: false, + currentArchivePresent: true, + baseIndex: index([]), + currentIndex: baseIndex, + baseFiles: new Map(), + currentFiles: baseFiles, + })).toEqual([]); + }); + + it('labels malformed base and current indexes before comparing bytes', () => { + expect(inspectPublicEconomicResilienceEvolution({ + baseArchivePresent: true, + currentArchivePresent: true, + baseIndex: {}, + currentIndex: {}, + baseFiles: new Map(), + currentFiles: new Map(), + }).join(' | ')).toContain('base archive: public economic resilience index fields drifted'); + expect(inspectPublicEconomicResilienceEvolution({ + baseArchivePresent: true, + currentArchivePresent: true, + baseIndex: {}, + currentIndex: {}, + baseFiles: new Map(), + currentFiles: new Map(), + }).join(' | ')).toContain('current archive: public economic resilience index fields drifted'); + }); +}); + +function check(overrides = {}) { + return inspectPublicEconomicResilienceEvolution({ + baseArchivePresent: true, + currentArchivePresent: true, + baseIndex, + currentIndex: baseIndex, + baseFiles, + currentFiles: new Map(baseFiles), + ...overrides, + }); +} diff --git a/test/publicEconomicResilienceZip.test.mjs b/test/publicEconomicResilienceZip.test.mjs new file mode 100644 index 0000000..a75403b --- /dev/null +++ b/test/publicEconomicResilienceZip.test.mjs @@ -0,0 +1,117 @@ +import { spawnSync } from 'node:child_process'; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + truncateSync, + unlinkSync, + writeFileSync, +} from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { + PUBLIC_ECONOMIC_RESILIENCE_MAX_MEMBER_BYTES, + PUBLIC_ECONOMIC_RESILIENCE_NATIVE_ARTIFACT_MEMBERS, + inspectPublicEconomicResilienceArtifactZip, +} from '../scripts/public-economic-resilience-zip.mjs'; + +const roots = []; + +afterEach(() => { + while (roots.length > 0) rmSync(roots.pop(), { recursive: true, force: true }); +}); + +describe('retained public economic artifact ZIP safety', () => { + it('accepts the exact regular member inventory and bounded CRC replay', () => { + const fixture = createZip(); + const report = inspectPublicEconomicResilienceArtifactZip(fixture.zip); + expect([...report.members.keys()].sort()).toEqual( + PUBLIC_ECONOMIC_RESILIENCE_NATIVE_ARTIFACT_MEMBERS + ); + expect(report.totalUncompressedBytes).toBeGreaterThan(0); + }); + + it('rejects extra members and symlink members', () => { + const extra = createZip({ extra: true }); + expect(() => inspectPublicEconomicResilienceArtifactZip(extra.zip)).toThrow( + 'member shape is invalid' + ); + + const linked = createNativeRoot(); + const target = path.join(path.dirname(linked.root), 'outside-source.jpg'); + writeFileSync(target, 'outside'); + const source = path.join(linked.root, 'source.jpg'); + unlinkSync(source); + symlinkSync(target, source); + const linkedZip = path.join(path.dirname(linked.root), 'linked.zip'); + zip(linked.root, linkedZip, ['-y']); + expect(() => inspectPublicEconomicResilienceArtifactZip(linkedZip)).toThrow( + 'not a regular file: source.jpg' + ); + }); + + it('rejects an oversized member before extraction and a CRC-corrupted member', () => { + const oversized = createNativeRoot(); + truncateSync( + path.join(oversized.root, 'source.jpg'), + PUBLIC_ECONOMIC_RESILIENCE_MAX_MEMBER_BYTES + 1 + ); + const oversizedZip = path.join(path.dirname(oversized.root), 'oversized.zip'); + zip(oversized.root, oversizedZip); + expect(() => inspectPublicEconomicResilienceArtifactZip(oversizedZip)).toThrow( + 'member size is invalid: source.jpg' + ); + + const corrupted = createZip({ stored: true }); + const bytes = readFileSync(corrupted.zip); + const payload = Buffer.from('payload-source.jpg'); + const offset = bytes.indexOf(payload); + expect(offset).toBeGreaterThan(0); + bytes[offset] ^= 0xff; + writeFileSync(corrupted.zip, bytes); + expect(() => inspectPublicEconomicResilienceArtifactZip(corrupted.zip)).toThrow( + 'artifact ZIP member source.jpg failed' + ); + }); +}); + +function createZip({ extra = false, stored = false } = {}) { + const fixture = createNativeRoot(); + if (extra) writeFileSync(path.join(fixture.root, 'extra.txt'), 'extra'); + const output = path.join(path.dirname(fixture.root), `artifact-${Date.now()}.zip`); + zip(fixture.root, output, [ + ...(stored ? ['-0'] : []), + ...(extra ? ['extra.txt'] : []), + ]); + return { ...fixture, zip: output }; +} + +function createNativeRoot() { + const parent = mkdtempSync(path.join(os.tmpdir(), 'rnick-public-zip-')); + roots.push(parent); + const root = path.join(parent, 'native'); + mkdirSync(root); + for (const member of PUBLIC_ECONOMIC_RESILIENCE_NATIVE_ARTIFACT_MEMBERS) { + const file = path.join(root, member); + mkdirSync(path.dirname(file), { recursive: true }); + writeFileSync(file, `payload-${member}`); + } + return { parent, root }; +} + +function zip(root, output, options = []) { + const extraMembers = options.filter((option) => !option.startsWith('-')); + const flags = options.filter((option) => option.startsWith('-')); + const result = spawnSync( + 'zip', + ['-q', '-X', ...flags, output, + ...PUBLIC_ECONOMIC_RESILIENCE_NATIVE_ARTIFACT_MEMBERS, + ...extraMembers], + { cwd: root, encoding: 'utf8' } + ); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error(result.stderr || result.stdout); +} diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index ef6934d..d65f69f 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -1,8 +1,15 @@ import { defineConfig } from 'vitepress'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { loadEconomicArchiveSnapshot } from './economicArchiveData'; const repository = 'https://github.com/GGULBAE/react-native-image-compression-kit'; const site = 'https://ggulbae.github.io/react-native-image-compression-kit/'; +const websiteRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..' +); export default defineConfig({ title: 'React Native Image Compression Kit', @@ -44,6 +51,10 @@ export default defineConfig({ 'link', { rel: 'canonical', href: new URL(route, site).toString() }, ]); + if (pageData.relativePath === 'reference/economic-resilience.md') { + pageData.frontmatter.economicArchive = + loadEconomicArchiveSnapshot(websiteRoot); + } }, themeConfig: { logo: '/logo.svg', @@ -53,6 +64,7 @@ export default defineConfig({ { text: 'API', link: '/reference/api' }, { text: 'Pipeline fit', link: '/guide/choosing-an-image-pipeline' }, { text: 'Why bytes matter', link: '/guide/byte-economics' }, + { text: 'Evidence archive', link: '/reference/economic-resilience' }, { text: 'Compatibility', link: '/reference/compatibility' }, { text: 'Roadmap', link: '/roadmap' }, { text: 'Changelog', link: '/changelog' }, @@ -84,6 +96,7 @@ export default defineConfig({ { text: 'Compatibility matrix', link: '/reference/compatibility' }, { text: 'Product architecture', link: '/reference/architecture' }, { text: 'Product evidence', link: '/reference/evidence' }, + { text: 'Economic resilience', link: '/reference/economic-resilience' }, { text: 'Roadmap', link: '/roadmap' }, { text: 'Native result demo', link: '/demo/' }, { text: 'Changelog', link: '/changelog' }, diff --git a/website/.vitepress/economicArchiveData.ts b/website/.vitepress/economicArchiveData.ts new file mode 100644 index 0000000..491539a --- /dev/null +++ b/website/.vitepress/economicArchiveData.ts @@ -0,0 +1,129 @@ +import { existsSync, lstatSync, readFileSync } from 'node:fs'; +import path from 'node:path'; + +export interface EconomicArchivePlatformSnapshot { + platform: 'android' | 'ios'; + root: string; + sourceBytes: number; + outputBytes: number; + sourceToOutputByteDifference: number; + uprightSimilarity: number; + verticalFlipSimilarity: number; + attemptedPackageOutputs: number; + removedPackageOutputs: number; + residualPackageOutputBytes: number; +} + +export type EconomicArchiveSnapshot = + | { kind: 'empty' } + | { + kind: 'available'; + count: number; + sourceCommit: string; + runId: number; + runAttempt: number; + runUrl: string; + platforms: EconomicArchivePlatformSnapshot[]; + }; + +export function loadEconomicArchiveSnapshot( + websiteRoot: string +): EconomicArchiveSnapshot { + const archiveRoot = path.join( + websiteRoot, + 'public/evidence/economic-resilience' + ); + const indexPath = path.join(archiveRoot, 'index.json'); + if (!existsSync(indexPath)) return { kind: 'empty' }; + const index = readJson(indexPath, 'economic archive index'); + if (!Array.isArray(index.captures)) { + throw new Error('economic archive index captures are invalid'); + } + if (index.captures.length === 0) return { kind: 'empty' }; + const latest = index.captures.at(-1); + if ( + !latest || + !/^[0-9a-f]{40}$/u.test(latest.sourceCommit ?? '') || + latest.captureSetPath !== + `source-tree/${latest.sourceCommit}/capture-set.json` + ) { + throw new Error('economic archive latest capture identity is invalid'); + } + const root = `source-tree/${latest.sourceCommit}`; + const captureSet = readJson( + path.join(archiveRoot, root, 'capture-set.json'), + 'economic archive capture-set' + ); + if ( + captureSet.sourceCommit !== latest.sourceCommit || + captureSet.workflow?.runId !== latest.runId || + !Number.isSafeInteger(captureSet.workflow?.runAttempt) || + captureSet.workflow.runAttempt <= 0 || + captureSet.workflow?.runUrl !== + `https://github.com/GGULBAE/react-native-image-compression-kit/actions/runs/${latest.runId}` + ) { + throw new Error('economic archive capture-set workflow identity is invalid'); + } + const platforms = (['android', 'ios'] as const).map((platform) => { + const platformRoot = path.join(archiveRoot, root, platform); + const evidence = readJson( + path.join(platformRoot, 'economic-resilience.json'), + `${platform} economic evidence` + ); + const visual = readJson( + path.join(platformRoot, 'visual-agreement.json'), + `${platform} visual agreement` + ); + for (const file of [ + path.join(platformRoot, 'source.jpg'), + path.join(platformRoot, 'output.jpg'), + path.join(archiveRoot, root, 'artifacts', `${platform}.zip`), + ]) { + assertRegularSingleLink(file, `${platform} public archive asset`); + } + const snapshot: EconomicArchivePlatformSnapshot = { + platform, + root, + sourceBytes: evidence.economics?.sourceBytes, + outputBytes: evidence.economics?.outputBytes, + sourceToOutputByteDifference: + evidence.economics?.sourceToOutputByteDifference, + uprightSimilarity: visual.uprightSimilarity, + verticalFlipSimilarity: visual.verticalFlipSimilarity, + attemptedPackageOutputs: evidence.cleanup?.attemptedPackageOutputs, + removedPackageOutputs: evidence.cleanup?.removedPackageOutputs, + residualPackageOutputBytes: + evidence.cleanup?.residualPackageOutputBytes, + }; + for (const [field, value] of Object.entries(snapshot)) { + if (field === 'platform' || field === 'root') continue; + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new Error(`${platform} archive snapshot field is invalid: ${field}`); + } + } + return snapshot; + }); + return { + kind: 'available', + count: index.captures.length, + sourceCommit: latest.sourceCommit, + runId: latest.runId, + runAttempt: captureSet.workflow.runAttempt, + runUrl: captureSet.workflow.runUrl, + platforms, + }; +} + +function readJson(file: string, label: string): any { + assertRegularSingleLink(file, label); + return JSON.parse(readFileSync(file, 'utf8')); +} + +function assertRegularSingleLink(file: string, label: string) { + const status = lstatSync(file); + if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1) { + throw new Error( + `${label} must be a regular non-symlink file with exactly one hard link` + ); + } +} diff --git a/website/.vitepress/theme/EconomicResilienceArchive.vue b/website/.vitepress/theme/EconomicResilienceArchive.vue new file mode 100644 index 0000000..e5ebb6e --- /dev/null +++ b/website/.vitepress/theme/EconomicResilienceArchive.vue @@ -0,0 +1,82 @@ + + + diff --git a/website/.vitepress/theme/custom.css b/website/.vitepress/theme/custom.css index e56819c..074c367 100644 --- a/website/.vitepress/theme/custom.css +++ b/website/.vitepress/theme/custom.css @@ -9,6 +9,13 @@ --vp-home-hero-image-filter: blur(48px); } +.dark { + --vp-c-brand-1: #5eead4; + --vp-c-brand-2: #2dd4bf; + --vp-c-brand-3: #14b8a6; + --vp-c-brand-soft: rgba(45, 212, 191, 0.16); +} + .vp-doc div[class*='language-'] { background-color: #0a0c10; } @@ -258,6 +265,207 @@ text-align: right; } +.economic-evidence-state { + display: inline-flex; + margin: 0.75rem 0 1.25rem; + border: 1px solid var(--vp-c-warning-2); + border-radius: 999px; + padding: 0.38rem 0.72rem; + background: var(--vp-c-warning-soft); + color: var(--vp-c-warning-1); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.02em; +} + +.economic-contract-grid, +.economic-proof-flow { + display: grid; + gap: 1rem; + margin: 1.4rem 0 1.75rem; +} + +.economic-contract-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.economic-contract-grid article, +.economic-proof-flow article { + border: 1px solid var(--vp-c-divider); + border-radius: 16px; + padding: 1rem; + background: linear-gradient(145deg, var(--vp-c-bg-soft), var(--vp-c-bg)); +} + +.economic-contract-grid span { + display: block; + color: var(--vp-c-text-2); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.economic-contract-grid strong { + display: block; + margin-top: 0.35rem; + color: var(--vp-c-text-1); + font-size: clamp(1.15rem, 2.4vw, 1.55rem); + font-variant-numeric: tabular-nums; +} + +.economic-contract-grid p, +.economic-proof-flow p { + margin: 0.45rem 0 0; + color: var(--vp-c-text-2); + font-size: 0.82rem; + line-height: 1.5; +} + +.economic-proof-flow { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.economic-proof-flow article { + position: relative; +} + +.economic-proof-flow span { + display: inline-grid; + width: 1.65rem; + height: 1.65rem; + margin-right: 0.4rem; + place-items: center; + border-radius: 50%; + background: var(--vp-c-brand-soft); + color: var(--vp-c-brand-1); + font-size: 0.75rem; + font-weight: 800; +} + +.economic-proof-flow strong { + color: var(--vp-c-text-1); +} + +.economic-archive-state { + margin: 1.25rem 0 2rem; + border: 1px solid var(--vp-c-divider); + border-radius: 20px; + padding: clamp(1rem, 3vw, 1.5rem); + background: linear-gradient(145deg, var(--vp-c-bg-soft), var(--vp-c-bg)); +} + +.economic-archive-state--empty p:last-child, +.economic-archive-boundary, +.economic-archive-sha { + color: var(--vp-c-text-2); +} + +.economic-archive-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} + +.economic-archive-heading h2 { + margin: 0; + border: 0; + padding: 0; +} + +.economic-archive-heading .economic-evidence-state { + margin: 0 0 0.65rem; + border-color: var(--vp-c-brand-2); + background: var(--vp-c-brand-soft); + color: var(--vp-c-brand-1); +} + +.economic-archive-sha code { + overflow-wrap: anywhere; +} + +.economic-observation-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + margin-top: 1.25rem; +} + +.economic-observation-grid article { + min-width: 0; + border: 1px solid var(--vp-c-divider); + border-radius: 16px; + overflow: hidden; + background: var(--vp-c-bg); +} + +.economic-observation-grid header, +.economic-observation-grid dl, +.economic-observation-grid nav { + padding: 1rem; +} + +.economic-observation-grid header span { + display: block; + color: var(--vp-c-brand-1); + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.economic-observation-grid header strong { + display: block; + margin-top: 0.2rem; + font-size: 1.35rem; + font-variant-numeric: tabular-nums; +} + +.economic-observation-grid header small { + color: var(--vp-c-text-2); +} + +.economic-observation-grid img { + display: block; + width: 100%; + aspect-ratio: 4 / 3; + object-fit: cover; + background: var(--vp-c-bg-soft); +} + +.economic-observation-grid dl { + display: grid; + gap: 0.6rem; + margin: 0; +} + +.economic-observation-grid dl div { + display: flex; + justify-content: space-between; + gap: 1rem; +} + +.economic-observation-grid dt { + color: var(--vp-c-text-2); +} + +.economic-observation-grid dd { + margin: 0; + color: var(--vp-c-text-1); + font-weight: 650; + text-align: right; + font-variant-numeric: tabular-nums; +} + +.economic-observation-grid nav { + display: flex; + flex-wrap: wrap; + gap: 0.55rem 0.9rem; + border-top: 1px solid var(--vp-c-divider); + font-size: 0.78rem; +} + .option-builder { margin: 3rem 0; border: 1px solid var(--vp-c-divider); @@ -564,12 +772,87 @@ .byte-cost-grid, .boundary-flow, .evidence-chart-grid, + .economic-contract-grid, + .economic-proof-flow, + .economic-observation-grid, .option-builder__layout, .byte-calculator__results, .result-grid { grid-template-columns: 1fr; } + .economic-archive-heading { + flex-direction: column; + } + + ._react-native-image-compression-kit_reference_economic-resilience table { + display: block; + border: 0; + } + + ._react-native-image-compression-kit_reference_economic-resilience thead { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; + } + + ._react-native-image-compression-kit_reference_economic-resilience tbody, + ._react-native-image-compression-kit_reference_economic-resilience tr, + ._react-native-image-compression-kit_reference_economic-resilience td { + display: block; + width: 100%; + } + + ._react-native-image-compression-kit_reference_economic-resilience tr { + margin-bottom: 0.85rem; + border: 1px solid var(--vp-c-divider); + border-radius: 14px; + overflow: hidden; + background: var(--vp-c-bg-soft); + } + + ._react-native-image-compression-kit_reference_economic-resilience td { + display: block; + border: 0; + border-bottom: 1px solid var(--vp-c-divider); + padding: 0.75rem; + } + + ._react-native-image-compression-kit_reference_economic-resilience td:last-child { + border-bottom: 0; + } + + ._react-native-image-compression-kit_reference_economic-resilience td::before { + display: block; + margin-bottom: 0.3rem; + color: var(--vp-c-text-2); + font-size: 0.72rem; + font-weight: 750; + letter-spacing: 0.04em; + text-transform: uppercase; + } + + ._react-native-image-compression-kit_reference_economic-resilience td:nth-child(1)::before { + content: 'Field'; + } + + ._react-native-image-compression-kit_reference_economic-resilience td:nth-child(2)::before { + content: 'Archive meaning'; + } + + ._react-native-image-compression-kit_reference_economic-resilience td:nth-child(3)::before { + content: 'Does not establish'; + } + + ._react-native-image-compression-kit_reference_economic-resilience td code { + overflow-wrap: anywhere; + word-break: break-word; + } + .byte-calculator__result-wide { grid-column: auto; } diff --git a/website/.vitepress/theme/index.ts b/website/.vitepress/theme/index.ts index 372b956..ac6bfbb 100644 --- a/website/.vitepress/theme/index.ts +++ b/website/.vitepress/theme/index.ts @@ -1,6 +1,7 @@ import DefaultTheme from 'vitepress/theme'; import type { Theme } from 'vitepress'; import ByteEconomicsCalculator from './ByteEconomicsCalculator.vue'; +import EconomicResilienceArchive from './EconomicResilienceArchive.vue'; import OptionBuilder from './OptionBuilder.vue'; import './custom.css'; @@ -8,6 +9,7 @@ export default { extends: DefaultTheme, enhanceApp({ app }) { app.component('ByteEconomicsCalculator', ByteEconomicsCalculator); + app.component('EconomicResilienceArchive', EconomicResilienceArchive); app.component('OptionBuilder', OptionBuilder); }, } satisfies Theme; diff --git a/website/public/evidence/economic-resilience/index.json b/website/public/evidence/economic-resilience/index.json new file mode 100644 index 0000000..7fd9376 --- /dev/null +++ b/website/public/evidence/economic-resilience/index.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "archive": "economic-resilience-source-tree-v1", + "captures": [ + { + "sourceCommit": "2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d", + "runId": 31701095800, + "captureSetPath": "source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/capture-set.json" + } + ] +} diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/economic-resilience.json b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/economic-resilience.json new file mode 100644 index 0000000..5014bab --- /dev/null +++ b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/economic-resilience.json @@ -0,0 +1,598 @@ +{ + "schemaVersion": 1, + "status": "passed", + "scenarioId": "kit-only-12mp-jpeg-v1", + "implementation": { + "name": "react-native-image-compression-kit", + "version": "0.4.1", + "buildSource": "checked-out-source-tree" + }, + "sourceCommit": "2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d", + "runId": 31701095800, + "runAttempt": 1, + "capturedAt": "2026-08-13T12:48:16.822Z", + "runUrl": "https://github.com/GGULBAE/react-native-image-compression-kit/actions/runs/31701095800", + "environment": { + "platform": "android", + "runtime": "Android 15 / API 35", + "osBuild": "AE3A.240806.043", + "device": "Google sdk_gphone64_x86_64", + "deviceKind": "emulator", + "abi": "x86_64", + "reactNativeArchitecture": "new", + "reactNativeVersion": "0.86.2", + "jsEngine": "hermes", + "buildType": "debug", + "runner": { + "label": "ubuntu-latest", + "os": "Linux", + "arch": "X64", + "name": "GitHub Actions 1000003256", + "imageOS": "ubuntu24", + "imageVersion": "20260720.247.2" + }, + "toolchain": { + "node": "v24.18.0", + "ffmpeg": "ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers", + "ffprobe": "ffprobe version 6.1.1-3ubuntu5 Copyright (c) 2007-2023 the FFmpeg developers", + "primary": "openjdk version \"21.0.11\" 2026-04-21 LTS", + "platformSdk": "Android compile SDK 36; emulator API 35; build-tools 36.0.0; NDK 27.1.12297006" + } + }, + "capabilities": { + "resourceLimits": { + "maxWorkingPixels": 25000000, + "maxSourcePixels": 100000000, + "maxSourceDimension": 32768 + }, + "maxConcurrentOperations": 2, + "supportsCancellation": true, + "supportsTargetSizeCompression": true, + "supportsDecodeDownsampling": true, + "formats": [ + { + "notes": [ + "Android MVP supports JPEG file:// and content:// sources.", + "EXIF orientation is applied before resize and selected output encoding.", + "Resize supports contain, cover, and stretch modes with maxWidth and maxHeight.", + "Target-size compression supports maxBytes by adjusting JPEG quality.", + "Metadata preserve copies supported JPEG source EXIF attributes into JPEG output.", + "Metadata safe copies privacy-filtered JPEG source EXIF attributes.", + "Metadata safe excludes GPS/location, owner/serial, maker note, user comment, and XMP.", + "Metadata preserve normalizes output EXIF orientation after pixels are transformed.", + "Metadata strip re-encodes JPEG output without preserving source metadata.", + "PNG, WebP, GIF, HEIC, HEIF, and AVIF sources are decoded without copying EXIF metadata." + ], + "supportsAnimation": false, + "supportsAlpha": false, + "format": "jpeg", + "input": true, + "output": true + }, + { + "notes": [ + "Android MVP supports PNG file:// and content:// sources.", + "Android can encode decoded JPEG, PNG, WebP, or GIF input to PNG output.", + "PNG output ignores quality and does not support target-size maxBytes.", + "Non-JPEG output does not preserve source EXIF metadata." + ], + "supportsAnimation": false, + "supportsAlpha": true, + "format": "png", + "input": true, + "output": true + }, + { + "notes": [ + "Android MVP supports WebP file:// and content:// sources.", + "Android can encode decoded JPEG, PNG, WebP, or GIF input to WebP output.", + "WebP target-size compression supports maxBytes by adjusting WebP quality.", + "Non-JPEG output does not preserve source EXIF metadata.", + "Animated WebP input or output is not implemented." + ], + "supportsAnimation": false, + "supportsAlpha": true, + "format": "webp", + "input": true, + "output": true + }, + { + "notes": [ + "HEIC input is supported on Android 8.0+ when device HEIF decode codecs are present.", + "Android API 28+ uses ImageDecoder for HEIC input.", + "Android API 26-27 attempts a guarded BitmapFactory HEIF decode fallback.", + "HEIC inputs are decoded without copying EXIF metadata.", + "HEIC output is not implemented." + ], + "supportsAnimation": false, + "supportsAlpha": false, + "format": "heic", + "input": true, + "output": false + }, + { + "notes": [ + "HEIF input is supported on Android 8.0+ when device HEIF decode codecs are present.", + "Android API 28+ uses ImageDecoder for HEIF input.", + "Android API 26-27 attempts a guarded BitmapFactory HEIF decode fallback.", + "HEIF inputs are decoded without copying EXIF metadata.", + "HEIF output is not implemented." + ], + "supportsAnimation": false, + "supportsAlpha": false, + "format": "heif", + "input": true, + "output": false + }, + { + "notes": [ + "AVIF input is supported on Android 14+ for baseline still images.", + "Android API 34+ uses ImageDecoder for AVIF input.", + "AVIF inputs are decoded without copying EXIF metadata.", + "Animated AVIF preservation is not implemented.", + "AVIF output is not implemented.", + "AVIF capability reports output=false; selecting output.format: 'avif' rejects with ERR_NOT_IMPLEMENTED.", + "Android AVIF output production wiring scaffold blocks entry into the extracted encode/decode-back helper while capability output=false.", + "Android AVIF output remains disabled until the MediaCodec image/avif encode/decode-back smoke produces a complete AVIF file with ftyp avif/avis signature and ImageDecoder decode-back validation.", + "metadata='preserve', output.maxBytes, and animated AVIF preservation remain unsupported for AVIF output until explicitly designed and tested." + ], + "supportsAnimation": false, + "supportsAlpha": false, + "format": "avif", + "input": true, + "output": false + }, + { + "notes": [ + "Android MVP decodes GIF file:// and content:// sources as a static first frame.", + "Animated GIF preservation is not implemented.", + "GIF output is not implemented.", + "GIF sources are decoded without copying EXIF metadata." + ], + "supportsAnimation": false, + "supportsAlpha": true, + "format": "gif", + "input": true, + "output": false + } + ], + "metadataPolicies": [ + "preserve", + "safe", + "strip" + ], + "platform": "android" + }, + "fixture": { + "id": "kit-only-12mp-v1", + "file": "source.jpg", + "manifestFile": "fixture-manifest.json", + "byteSize": 1721333, + "sha256": "bdcf4e083f1860d8829898211e4b1c428a80dfd53dceca697c6f7e4a4901bfcc", + "width": 4000, + "height": 3000, + "remainsAfterRun": true + }, + "operation": { + "resize": { + "maxWidth": 1600, + "maxHeight": 1200, + "mode": "contain" + }, + "output": { + "format": "jpeg", + "quality": 90, + "maxBytes": 500000 + }, + "metadata": "strip" + }, + "timing": { + "clock": "performance.now", + "boundary": "compressImage-call-only", + "warmupIterations": 2, + "measuredIterations": 10 + }, + "samples": [ + { + "phase": "warmup", + "iteration": 1, + "elapsedMs": 62.19035400002031, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "warmup", + "iteration": 2, + "elapsedMs": 61.75065199995879, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 1, + "elapsedMs": 60.22498800000176, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 2, + "elapsedMs": 60.822243999980856, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 3, + "elapsedMs": 61.220919999992475, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 4, + "elapsedMs": 64.13922299997648, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 5, + "elapsedMs": 60.814517999999225, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 6, + "elapsedMs": 60.95991899998626, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 7, + "elapsedMs": 61.15849000000162, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 8, + "elapsedMs": 60.586523000034504, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 9, + "elapsedMs": 60.65781699999934, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 10, + "elapsedMs": 66.18564900005003, + "result": { + "compressionRatio": 0.13195529278762447, + "originalByteSize": 1721333, + "byteSize": 227139, + "height": 1200, + "format": "jpeg", + "width": 1600 + }, + "sourceToOutputByteDifference": 1494194, + "outputInspection": { + "exists": true, + "width": 1600, + "mediaType": "image/jpeg", + "height": 1200, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "byteSize": 227139 + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + } + ], + "measuredSummary": { + "elapsedMs": { + "min": 60.225, + "median": 60.891, + "p95": 66.186, + "max": 66.186 + }, + "outputBytes": { + "min": 227139, + "median": 227139, + "max": 227139 + }, + "dimensions": [ + { + "width": 1600, + "height": 1200, + "count": 10 + } + ] + }, + "representative": { + "measuredIteration": 10, + "file": "output.jpg", + "byteSize": 227139, + "sha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c", + "width": 1600, + "height": 1200 + }, + "economics": { + "boundary": "source-to-output-observation", + "sourceOwnership": "source-remains", + "stagedEvidenceOwnership": "example-owned-copy", + "matchedTransferBaseline": null, + "sourceBytes": 1721333, + "outputBytes": 227139, + "sourceToOutputByteDifference": 1494194, + "costSavingsClaim": null + }, + "cleanup": { + "attemptedPackageOutputs": 12, + "removedPackageOutputs": 12, + "residualPackageOutputs": 0, + "residualPackageOutputBytes": 0 + }, + "visualAgreement": { + "schemaVersion": 3, + "status": "passed", + "algorithm": "ffmpeg-auto-oriented-contain-limited-range-ssim-v3", + "comparisonProfile": "jpeg-full-range-to-limited-yuv444p-v1", + "inputColorRange": "pc", + "comparisonColorRange": "tv", + "comparisonPixelFormat": "yuv444p", + "comparisonScaler": "lanczos", + "scoreTolerance": 0.001, + "sourceColorRange": "pc", + "outputColorRange": "pc", + "resizeMode": "contain", + "maxWidth": 1600, + "maxHeight": 1200, + "sourceWidth": 4000, + "sourceHeight": 3000, + "expectedWidth": 1600, + "expectedHeight": 1200, + "width": 1600, + "height": 1200, + "uprightSimilarity": 0.944431, + "verticalFlipSimilarity": 0.690423, + "minimumSimilarity": 0.9, + "minimumOrientationMargin": 0.02, + "checks": { + "geometry": true, + "minimumSimilarity": true, + "orientationMargin": true + }, + "sourceSha256": "bdcf4e083f1860d8829898211e4b1c428a80dfd53dceca697c6f7e4a4901bfcc", + "outputSha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c" + } +} diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/environment.json b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/environment.json new file mode 100644 index 0000000..0ff0b77 --- /dev/null +++ b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/environment.json @@ -0,0 +1,27 @@ +{ + "platform": "android", + "runtime": "Android 15 / API 35", + "osBuild": "AE3A.240806.043", + "device": "Google sdk_gphone64_x86_64", + "deviceKind": "emulator", + "abi": "x86_64", + "reactNativeArchitecture": "new", + "reactNativeVersion": "0.86.2", + "jsEngine": "hermes", + "buildType": "debug", + "runner": { + "label": "ubuntu-latest", + "os": "Linux", + "arch": "X64", + "name": "GitHub Actions 1000003256", + "imageOS": "ubuntu24", + "imageVersion": "20260720.247.2" + }, + "toolchain": { + "node": "v24.18.0", + "ffmpeg": "ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers", + "ffprobe": "ffprobe version 6.1.1-3ubuntu5 Copyright (c) 2007-2023 the FFmpeg developers", + "primary": "openjdk version \"21.0.11\" 2026-04-21 LTS", + "platformSdk": "Android compile SDK 36; emulator API 35; build-tools 36.0.0; NDK 27.1.12297006" + } +} diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/fixture-manifest.json b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/fixture-manifest.json new file mode 100644 index 0000000..cfa3a53 --- /dev/null +++ b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/fixture-manifest.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "id": "kit-only-12mp-v1", + "file": "kit-only-12mp-v1.jpg", + "provenance": { + "kind": "project-generated-synthetic", + "containsPersonalData": false, + "license": "MIT", + "generator": "FFmpeg 8.1.2 and libjpeg-turbo jpegtran 3.1.4.1", + "recipe": "testsrc2 4000x3000 with three asymmetric color fields and a 127x113 grid; MJPEG q=2, yuvj444p, bitexact muxing; jpegtran -copy none -optimize removes COM, EXIF, XMP, and IPTC metadata" + }, + "mediaType": "image/jpeg", + "width": 4000, + "height": 3000, + "pixelCount": 12000000, + "orientation": 1, + "orientationEncoding": "implicit-default-no-exif-orientation", + "byteSize": 1721333, + "maximumFixtureByteSize": 8000000, + "sha256": "bdcf4e083f1860d8829898211e4b1c428a80dfd53dceca697c6f7e4a4901bfcc" +} diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/output.jpg b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/output.jpg new file mode 100644 index 0000000..63d69aa Binary files /dev/null and b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/output.jpg differ diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/source.jpg b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/source.jpg new file mode 100644 index 0000000..6c30abf Binary files /dev/null and b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/source.jpg differ diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/visual-agreement.json b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/visual-agreement.json new file mode 100644 index 0000000..17db635 --- /dev/null +++ b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/android/visual-agreement.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 3, + "status": "passed", + "algorithm": "ffmpeg-auto-oriented-contain-limited-range-ssim-v3", + "comparisonProfile": "jpeg-full-range-to-limited-yuv444p-v1", + "inputColorRange": "pc", + "comparisonColorRange": "tv", + "comparisonPixelFormat": "yuv444p", + "comparisonScaler": "lanczos", + "scoreTolerance": 0.001, + "sourceColorRange": "pc", + "outputColorRange": "pc", + "resizeMode": "contain", + "maxWidth": 1600, + "maxHeight": 1200, + "sourceWidth": 4000, + "sourceHeight": 3000, + "expectedWidth": 1600, + "expectedHeight": 1200, + "width": 1600, + "height": 1200, + "uprightSimilarity": 0.944431, + "verticalFlipSimilarity": 0.690423, + "minimumSimilarity": 0.9, + "minimumOrientationMargin": 0.02, + "checks": { + "geometry": true, + "minimumSimilarity": true, + "orientationMargin": true + }, + "sourceSha256": "bdcf4e083f1860d8829898211e4b1c428a80dfd53dceca697c6f7e4a4901bfcc", + "outputSha256": "af752a5ad998e6030a7bba11fbd6d45f99f495fc7c3ba3173d2ee9ffd0cd918c" +} diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/artifact-metadata.json b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/artifact-metadata.json new file mode 100644 index 0000000..e37bbf1 --- /dev/null +++ b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/artifact-metadata.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "runId": 31701095800, + "artifacts": [ + { + "platform": "android", + "id": 9181519094, + "name": "native-demo-android-2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d", + "digest": "sha256:6aacf829a8b5fd4a15dc45f78825b20d7eb94ac7e0a6ea05df62dd89e8059fb5", + "expired": false, + "sizeInBytes": 1734370, + "archiveDownloadUrl": "https://api.github.com/repos/GGULBAE/react-native-image-compression-kit/actions/artifacts/9181519094/zip" + }, + { + "platform": "ios", + "id": 9181900101, + "name": "native-demo-ios-2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d", + "digest": "sha256:d13366ee4d662486350a671fce64550d0c7b13889ceb2aacac29c070ee601d65", + "expired": false, + "sizeInBytes": 1891857, + "archiveDownloadUrl": "https://api.github.com/repos/GGULBAE/react-native-image-compression-kit/actions/artifacts/9181900101/zip" + } + ] +} diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/artifacts/android.zip b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/artifacts/android.zip new file mode 100644 index 0000000..b5459a9 Binary files /dev/null and b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/artifacts/android.zip differ diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/artifacts/ios.zip b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/artifacts/ios.zip new file mode 100644 index 0000000..1e9244a Binary files /dev/null and b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/artifacts/ios.zip differ diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/capture-set.json b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/capture-set.json new file mode 100644 index 0000000..91ae79e --- /dev/null +++ b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/capture-set.json @@ -0,0 +1,50 @@ +{ + "schemaVersion": 1, + "status": "passed", + "scenarioId": "kit-only-12mp-jpeg-v1", + "sourceCommit": "2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d", + "artifactMetadata": { + "file": "artifact-metadata.json", + "sha256": "e56481b5359ae7062e9873da5b7906352ed841e7f5c3508b26ca28fa10ca6b8b" + }, + "workflow": { + "metadataFile": "run-metadata.json", + "metadataSha256": "eaf29c17f58ed36fe70a304fe0040fda44e048acd87e9d653d163fbfc2f864de", + "file": ".github/workflows/demo-evidence.yml", + "workflowName": "Native Demo Evidence", + "eventName": "workflow_dispatch", + "headBranch": "master", + "ref": "refs/heads/master", + "headSha": "2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d", + "runId": 31701095800, + "runAttempt": 1, + "runUrl": "https://github.com/GGULBAE/react-native-image-compression-kit/actions/runs/31701095800" + }, + "platforms": { + "android": { + "artifactPath": "android", + "artifactArchivePath": "artifacts/android.zip", + "artifactId": 9181519094, + "artifactName": "native-demo-android-2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d", + "artifactDigest": "sha256:6aacf829a8b5fd4a15dc45f78825b20d7eb94ac7e0a6ea05df62dd89e8059fb5", + "artifactExpired": false, + "artifactSizeInBytes": 1734370 + }, + "ios": { + "artifactPath": "ios", + "artifactArchivePath": "artifacts/ios.zip", + "artifactId": 9181900101, + "artifactName": "native-demo-ios-2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d", + "artifactDigest": "sha256:d13366ee4d662486350a671fce64550d0c7b13889ceb2aacac29c070ee601d65", + "artifactExpired": false, + "artifactSizeInBytes": 1891857 + } + }, + "claimBoundary": { + "observation": "source-to-output-byte-difference", + "sourceOwnership": "source-remains", + "matchedTransferBaseline": null, + "costSavingsClaim": null, + "crossPlatformTimingComparison": null + } +} diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/economic-resilience.json b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/economic-resilience.json new file mode 100644 index 0000000..9654806 --- /dev/null +++ b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/economic-resilience.json @@ -0,0 +1,590 @@ +{ + "schemaVersion": 1, + "status": "passed", + "scenarioId": "kit-only-12mp-jpeg-v1", + "implementation": { + "name": "react-native-image-compression-kit", + "version": "0.4.1", + "buildSource": "checked-out-source-tree" + }, + "sourceCommit": "2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d", + "runId": 31701095800, + "runAttempt": 1, + "capturedAt": "2026-08-13T13:00:09.118Z", + "runUrl": "https://github.com/GGULBAE/react-native-image-compression-kit/actions/runs/31701095800", + "environment": { + "platform": "ios", + "runtime": "iOS 26.4", + "osBuild": "23E254a", + "device": "iPhone 17 Pro", + "deviceKind": "simulator", + "abi": "arm64", + "reactNativeArchitecture": "legacy", + "reactNativeVersion": "0.86.2", + "jsEngine": "hermes", + "buildType": "debug", + "runner": { + "label": "macos-latest", + "os": "macOS", + "arch": "ARM64", + "name": "GitHub Actions 1000003255", + "imageOS": "macos26", + "imageVersion": "20260728.0273.1" + }, + "toolchain": { + "node": "v24.18.0", + "ffmpeg": "ffmpeg version 8.1.2 Copyright (c) 2000-2026 the FFmpeg developers", + "ffprobe": "ffprobe version 8.1.2 Copyright (c) 2007-2026 the FFmpeg developers", + "primary": "Xcode 26.6 Build version 17F113", + "platformSdk": "iOS Simulator 26.5" + } + }, + "capabilities": { + "resourceLimits": { + "maxSourceDimension": 32768, + "maxSourcePixels": 100000000, + "maxWorkingPixels": 25000000 + }, + "supportsDecodeDownsampling": true, + "platform": "ios", + "metadataPolicies": [ + "preserve", + "safe", + "strip" + ], + "formats": [ + { + "notes": [ + "iOS supports JPEG input and JPEG output through background ImageIO/CoreGraphics work.", + "Large resize requests use ImageIO thumbnail downsampling before transform allocation.", + "JPEG output supports quality-based compression and optional resize.", + "Target-size compression supports maxBytes by adjusting JPEG quality.", + "Metadata preserve copies source JPEG metadata and normalizes output orientation/dimensions for JPEG input to JPEG output.", + "Metadata safe and strip re-encode without copying source metadata.", + "Non-JPEG input or non-JPEG output rejects metadata preserve with ERR_NOT_IMPLEMENTED." + ], + "supportsAnimation": false, + "input": true, + "output": true, + "supportsAlpha": false, + "format": "jpeg" + }, + { + "notes": [ + "iOS supports PNG input and PNG output through background ImageIO/CoreGraphics work.", + "PNG output preserves alpha where the processed image contains transparency.", + "PNG output ignores quality and does not support target-size maxBytes." + ], + "supportsAnimation": false, + "input": true, + "output": true, + "supportsAlpha": true, + "format": "png" + }, + { + "notes": [ + "iOS MVP decodes WebP input as a static first frame through ImageIO.", + "WebP input can be re-encoded to JPEG or PNG output without copying source metadata.", + "This runtime does not advertise ImageIO WebP destination encoding support.", + "Runtime-available WebP output supports target-size maxBytes by adjusting WebP quality.", + "Animated WebP preservation is not implemented." + ], + "supportsAnimation": false, + "input": true, + "output": false, + "supportsAlpha": true, + "format": "webp" + }, + { + "notes": [ + "iOS MVP decodes HEIC input as a static image through ImageIO.", + "HEIC input can be re-encoded to JPEG or PNG output without copying source metadata.", + "WebP output still requires runtime ImageIO WebP destination support.", + "HEIC output is not implemented." + ], + "supportsAnimation": false, + "input": true, + "output": false, + "supportsAlpha": true, + "format": "heic" + }, + { + "notes": [ + "iOS MVP decodes HEIF input as a static image through ImageIO.", + "HEIF input can be re-encoded to JPEG or PNG output without copying source metadata.", + "WebP output still requires runtime ImageIO WebP destination support.", + "HEIF output is not implemented." + ], + "supportsAnimation": false, + "input": true, + "output": false, + "supportsAlpha": true, + "format": "heif" + }, + { + "notes": [ + "This runtime advertises ImageIO AVIF source support, so iOS MVP decodes AVIF input as a static image through ImageIO.", + "AVIF input can be re-encoded to JPEG or PNG output without copying source metadata.", + "WebP output still requires runtime ImageIO WebP destination support.", + "Animated AVIFpreservation is not implemented.", + "AVIF output is not implemented.", + "AVIF capability reports output=false; selecting output.format: 'avif' rejects with ERR_NOT_IMPLEMENTED.", + "Future iOS AVIF output must be runtime-gated by ImageIO AVIF destination support and static output validation.", + "metadata='preserve', output.maxBytes, and animated AVIF preservation remain unsupported for AVIF output until explicitly designed and tested." + ], + "supportsAnimation": false, + "input": true, + "output": false, + "supportsAlpha": true, + "format": "avif" + }, + { + "notes": [ + "iOS MVP decodes GIF input as a static first frame through ImageIO.", + "GIF input can be re-encoded to JPEG or PNG output without copying source metadata.", + "Animated GIF preservation and GIF output are not implemented." + ], + "supportsAnimation": false, + "input": true, + "output": false, + "supportsAlpha": true, + "format": "gif" + } + ], + "supportsCancellation": true, + "maxConcurrentOperations": 2, + "supportsTargetSizeCompression": true + }, + "fixture": { + "id": "kit-only-12mp-v1", + "file": "source.jpg", + "manifestFile": "fixture-manifest.json", + "byteSize": 1721333, + "sha256": "bdcf4e083f1860d8829898211e4b1c428a80dfd53dceca697c6f7e4a4901bfcc", + "width": 4000, + "height": 3000, + "remainsAfterRun": true + }, + "operation": { + "resize": { + "maxWidth": 1600, + "maxHeight": 1200, + "mode": "contain" + }, + "output": { + "format": "jpeg", + "quality": 90, + "maxBytes": 500000 + }, + "metadata": "strip" + }, + "timing": { + "clock": "performance.now", + "boundary": "compressImage-call-only", + "warmupIterations": 2, + "measuredIterations": 10 + }, + "samples": [ + { + "phase": "warmup", + "iteration": 1, + "elapsedMs": 110.31516700005159, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "warmup", + "iteration": 2, + "elapsedMs": 84.76220800005831, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 1, + "elapsedMs": 81.69066600012593, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 2, + "elapsedMs": 78.38341599982232, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 3, + "elapsedMs": 71.97691600001417, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 4, + "elapsedMs": 101.56304099992849, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 5, + "elapsedMs": 81.48999999999069, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 6, + "elapsedMs": 84.07125000003725, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 7, + "elapsedMs": 78.91512499982491, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 8, + "elapsedMs": 104.43079199991189, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 9, + "elapsedMs": 75.90279200021178, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + }, + { + "phase": "measured", + "iteration": 10, + "elapsedMs": 73.69616700010374, + "result": { + "format": "jpeg", + "height": 1200, + "originalByteSize": 1721333, + "width": 1600, + "byteSize": 372454, + "compressionRatio": 0.2163753323732247 + }, + "sourceToOutputByteDifference": 1348879, + "outputInspection": { + "height": 1200, + "mediaType": "image/jpeg", + "exists": true, + "byteSize": 372454, + "width": 1600, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + }, + "cleanup": { + "packageOutputRemoved": true, + "existsAfterRemoval": false, + "residualByteSize": 0 + } + } + ], + "measuredSummary": { + "elapsedMs": { + "min": 71.977, + "median": 80.203, + "p95": 104.431, + "max": 104.431 + }, + "outputBytes": { + "min": 372454, + "median": 372454, + "max": 372454 + }, + "dimensions": [ + { + "width": 1600, + "height": 1200, + "count": 10 + } + ] + }, + "representative": { + "measuredIteration": 10, + "file": "output.jpg", + "byteSize": 372454, + "sha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8", + "width": 1600, + "height": 1200 + }, + "economics": { + "boundary": "source-to-output-observation", + "sourceOwnership": "source-remains", + "stagedEvidenceOwnership": "example-owned-copy", + "matchedTransferBaseline": null, + "sourceBytes": 1721333, + "outputBytes": 372454, + "sourceToOutputByteDifference": 1348879, + "costSavingsClaim": null + }, + "cleanup": { + "attemptedPackageOutputs": 12, + "removedPackageOutputs": 12, + "residualPackageOutputs": 0, + "residualPackageOutputBytes": 0 + }, + "visualAgreement": { + "schemaVersion": 3, + "status": "passed", + "algorithm": "ffmpeg-auto-oriented-contain-limited-range-ssim-v3", + "comparisonProfile": "jpeg-full-range-to-limited-yuv444p-v1", + "inputColorRange": "pc", + "comparisonColorRange": "tv", + "comparisonPixelFormat": "yuv444p", + "comparisonScaler": "lanczos", + "scoreTolerance": 0.001, + "sourceColorRange": "pc", + "outputColorRange": "pc", + "resizeMode": "contain", + "maxWidth": 1600, + "maxHeight": 1200, + "sourceWidth": 4000, + "sourceHeight": 3000, + "expectedWidth": 1600, + "expectedHeight": 1200, + "width": 1600, + "height": 1200, + "uprightSimilarity": 0.951897, + "verticalFlipSimilarity": 0.690032, + "minimumSimilarity": 0.9, + "minimumOrientationMargin": 0.02, + "checks": { + "geometry": true, + "minimumSimilarity": true, + "orientationMargin": true + }, + "sourceSha256": "bdcf4e083f1860d8829898211e4b1c428a80dfd53dceca697c6f7e4a4901bfcc", + "outputSha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" + } +} diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/environment.json b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/environment.json new file mode 100644 index 0000000..7de4eb1 --- /dev/null +++ b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/environment.json @@ -0,0 +1,27 @@ +{ + "platform": "ios", + "runtime": "iOS 26.4", + "osBuild": "23E254a", + "device": "iPhone 17 Pro", + "deviceKind": "simulator", + "abi": "arm64", + "reactNativeArchitecture": "legacy", + "reactNativeVersion": "0.86.2", + "jsEngine": "hermes", + "buildType": "debug", + "runner": { + "label": "macos-latest", + "os": "macOS", + "arch": "ARM64", + "name": "GitHub Actions 1000003255", + "imageOS": "macos26", + "imageVersion": "20260728.0273.1" + }, + "toolchain": { + "node": "v24.18.0", + "ffmpeg": "ffmpeg version 8.1.2 Copyright (c) 2000-2026 the FFmpeg developers", + "ffprobe": "ffprobe version 8.1.2 Copyright (c) 2007-2026 the FFmpeg developers", + "primary": "Xcode 26.6 Build version 17F113", + "platformSdk": "iOS Simulator 26.5" + } +} diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/fixture-manifest.json b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/fixture-manifest.json new file mode 100644 index 0000000..cfa3a53 --- /dev/null +++ b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/fixture-manifest.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "id": "kit-only-12mp-v1", + "file": "kit-only-12mp-v1.jpg", + "provenance": { + "kind": "project-generated-synthetic", + "containsPersonalData": false, + "license": "MIT", + "generator": "FFmpeg 8.1.2 and libjpeg-turbo jpegtran 3.1.4.1", + "recipe": "testsrc2 4000x3000 with three asymmetric color fields and a 127x113 grid; MJPEG q=2, yuvj444p, bitexact muxing; jpegtran -copy none -optimize removes COM, EXIF, XMP, and IPTC metadata" + }, + "mediaType": "image/jpeg", + "width": 4000, + "height": 3000, + "pixelCount": 12000000, + "orientation": 1, + "orientationEncoding": "implicit-default-no-exif-orientation", + "byteSize": 1721333, + "maximumFixtureByteSize": 8000000, + "sha256": "bdcf4e083f1860d8829898211e4b1c428a80dfd53dceca697c6f7e4a4901bfcc" +} diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/output.jpg b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/output.jpg new file mode 100644 index 0000000..78441d5 Binary files /dev/null and b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/output.jpg differ diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/source.jpg b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/source.jpg new file mode 100644 index 0000000..6c30abf Binary files /dev/null and b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/source.jpg differ diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/visual-agreement.json b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/visual-agreement.json new file mode 100644 index 0000000..fe39c8f --- /dev/null +++ b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/ios/visual-agreement.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 3, + "status": "passed", + "algorithm": "ffmpeg-auto-oriented-contain-limited-range-ssim-v3", + "comparisonProfile": "jpeg-full-range-to-limited-yuv444p-v1", + "inputColorRange": "pc", + "comparisonColorRange": "tv", + "comparisonPixelFormat": "yuv444p", + "comparisonScaler": "lanczos", + "scoreTolerance": 0.001, + "sourceColorRange": "pc", + "outputColorRange": "pc", + "resizeMode": "contain", + "maxWidth": 1600, + "maxHeight": 1200, + "sourceWidth": 4000, + "sourceHeight": 3000, + "expectedWidth": 1600, + "expectedHeight": 1200, + "width": 1600, + "height": 1200, + "uprightSimilarity": 0.951897, + "verticalFlipSimilarity": 0.690032, + "minimumSimilarity": 0.9, + "minimumOrientationMargin": 0.02, + "checks": { + "geometry": true, + "minimumSimilarity": true, + "orientationMargin": true + }, + "sourceSha256": "bdcf4e083f1860d8829898211e4b1c428a80dfd53dceca697c6f7e4a4901bfcc", + "outputSha256": "ffa772d189d0f62bb48cb40cf801c17298db8fb453279a559302e7567ab367a8" +} diff --git a/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/run-metadata.json b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/run-metadata.json new file mode 100644 index 0000000..5edaeb5 --- /dev/null +++ b/website/public/evidence/economic-resilience/source-tree/2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d/run-metadata.json @@ -0,0 +1,11 @@ +{ + "event": "workflow_dispatch", + "headBranch": "master", + "headSha": "2a2ef744d7b2cd90cd9f42a8bf392cd19676a44d", + "conclusion": "success", + "workflowName": "Native Demo Evidence", + "path": ".github/workflows/demo-evidence.yml", + "runId": 31701095800, + "attempt": 1, + "url": "https://github.com/GGULBAE/react-native-image-compression-kit/actions/runs/31701095800" +} diff --git a/website/reference/economic-resilience.md b/website/reference/economic-resilience.md new file mode 100644 index 0000000..4404bdc --- /dev/null +++ b/website/reference/economic-resilience.md @@ -0,0 +1,126 @@ +--- +description: Reproducible source-tree evidence for large-photo byte, visual-integrity, and output-lifecycle contracts. +--- + +# Economic resilience evidence + + + +The scenario asks a practical integration question: when an application sends +a large JPEG through the package, which byte, visual, and file-lifecycle facts +can be reproduced without turning one hosted runner into a universal economic +claim? + +
+
+ Input contract + 12 MP JPEG +

Project-generated 4,000 × 3,000 fixture with a fixed SHA-256 identity.

+
+
+ Output request + 1,600 × 1,200 +

Contain resize, JPEG quality 90, metadata strip, and a 500,000 B ceiling.

+
+
+ Lifecycle gate + 0 B residual +

Two warmups and ten measured package outputs must leave zero residual bytes.

+
+
+ +These values define the fixture and acceptance plan. The archive displays them +as observations only after Android and iOS artifacts from the same accepted +workflow run are stored and independently replayed. + +## What a capture must prove + +
+
1Bind

Exact master SHA, workflow_dispatch run, attempt, environment, and capabilities.

+
2Inspect

Source and output bytes, JPEG geometry, stripped metadata, and cleanup state.

+
3Replay

Portable FFmpeg schema 3 SSIM plus an explicit vertical-flip control.

+
4Limit

Keep source/output difference separate from transfer, storage, and currency claims.

+
+ +Each platform artifact must contain exactly six regular, non-symlink files +whose hard-link count is exactly one: +`economic-resilience.json`, `environment.json`, `fixture-manifest.json`, +`source.jpg`, `output.jpg`, and `visual-agreement.json`. Android and iOS must +carry byte-identical source and fixture-manifest files and identify the same +full source SHA, workflow run ID, and run attempt. +The retained run must resolve to `refs/heads/master`; the importer derives this +identity from GitHub rather than accepting a caller-supplied branch constant. + +The visual gate independently decodes the retained JPEGs. Both the captured +and replayed reports must satisfy contain geometry, upright SSIM of at least +0.90, and an upright-over-vertical-flip margin of at least 0.02. Schema 3 pins +the full-range JPEG conversion, limited-range `yuv444p` comparison surface, +Lanczos scaler, and a maximum replay score tolerance of 0.001. + +## Economic claim boundary + +| Field | Archive meaning | What it does not establish | +| --- | --- | --- | +| Source bytes | Bytes of the retained fixture entering this package call | Upstream transfer volume | +| Output bytes | Bytes of measured iteration 10 | Bytes accepted or retained by a production backend | +| Signed byte difference | `source bytes - output bytes` for this one operation | Avoided transfer or storage | +| `source-remains` | The source still exists after the package run | App-owned source replacement | +| `matchedTransferBaseline: null` | No matched current-pipeline transfer baseline was measured | Transfer savings | +| `costSavingsClaim: null` | No price and retention model is bound to the capture | Currency savings | +| Raw timing samples | Environment-specific call-only observations | A platform comparison or speed ranking | + +The signed byte difference can be useful when planning a measurement, but it +is not money. A defensible cost estimate still needs accepted-output counts, +an app-owned retention boundary, a matched transfer baseline, and dated prices. +Use the [byte economics guide](../guide/byte-economics.md) to model those inputs +separately. + +## Append-only archive layout + +Captures are addressed by the full source SHA. There is no mutable `latest` +alias, and importing the same SHA twice is rejected. + +```text +website/public/evidence/economic-resilience/ +├── index.json +└── source-tree/ + └── <40-character-source-sha>/ + ├── capture-set.json + ├── run-metadata.json + ├── artifact-metadata.json + ├── artifacts/ + │ ├── android.zip + │ └── ios.zip + ├── android/ # exact six-file artifact + └── ios/ # exact six-file artifact +``` + +The archive verifier rejects path traversal, linked files or directories, +unexpected assets, SHA-to-directory mismatches, overwrites, unindexed capture +directories, and index entries without a corresponding capture directory. + +## Import and verify + +Run the independent verifier before reviewing an archive change: + +```bash +pnpm import:public-economic-resilience-evidence -- \ + --run-id + +pnpm verify:public-economic-resilience-evidence +pnpm verify:public-economic-resilience-evolution -- --base +``` + +The importer queries the GitHub run and artifact APIs itself, downloads the two +artifacts by immutable ID, binds their API digest and size, and retains both +original ZIPs beside the exact six extracted files. It verifies bounded ZIP +member inventories and CRCs, rechecks cross-platform identity, refuses +replacement, and uses a recoverable journal for capture/index publication. +The verifier rechecks ZIP-to-file equality and replays every retained platform +bundle using local `zipinfo`, `unzip`, FFmpeg, and ffprobe tools. CI also compares +the base revision so every earlier capture and retained byte remains unchanged; +for each new suffix entry, it reacquires the GitHub run and artifacts and +requires the retained metadata and ZIP bytes to match the API response exactly. + +For the native capture boundary and raw-sample format, see the +[12 MP benchmark methodology](https://github.com/GGULBAE/react-native-image-compression-kit/blob/master/docs/benchmarks/README.md#12-mp-kit-only-economic-resilience). diff --git a/website/reference/evidence.md b/website/reference/evidence.md index c6c5098..2842c82 100644 --- a/website/reference/evidence.md +++ b/website/reference/evidence.md @@ -207,6 +207,11 @@ source remains and no matched baseline is supplied. See the [benchmark methodology](https://github.com/GGULBAE/react-native-image-compression-kit/blob/master/docs/benchmarks/README.md#12-mp-kit-only-economic-resilience) for the complete timing boundary, artifact verifier, and interpretation limits. +The [economic resilience archive](./economic-resilience.md) documents the +append-only source-tree preservation contract and shows its claim boundaries. +It renders a methodology preview while the index is empty, then renders only +archive-derived bytes, visual scores, lifecycle facts, retained assets, and run +provenance after an exact-master capture is present. ## Reproduce the evidence