{"record":{"id":"8e6b025e8e782843","repo":"affaan-m/ECC","slug":"file-changed-while-hashing-path","errorCode":null,"errorMessage":"File changed while hashing: {path}","messagePattern":"File changed while hashing: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/assets.py","lineNumber":67,"sourceCode":"        flags = os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0) | os.O_NONBLOCK\n        fd = os.open(path, flags)\n        with os.fdopen(fd, 'rb') as stream:\n            opened = os.fstat(stream.fileno())\n            if not stat.S_ISREG(opened.st_mode):\n                raise ValueError(f'Not a regular file: {path}')\n            digest = hashlib.sha256()\n            header = stream.read(12)\n            digest.update(header)\n            for chunk in iter(lambda: stream.read(1024 * 1024), b''):\n                digest.update(chunk)\n            after = os.fstat(stream.fileno())\n        final = path.stat()\n    except OSError as exc:\n        raise ValueError(f'Cannot read local asset: {path}') from exc\n    def identity(info: os.stat_result) -> tuple[int, ...]:\n        return (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns)\n    if len({identity(info) for info in (before, opened, after, final)}) != 1:\n        raise ValueError(f'File changed while hashing: {path}')\n    if modality == '3d_asset':\n        if len(header) != 12:\n            raise ValueError(f'Truncated GLB header: {path}')\n        magic, version, size = struct.unpack('<4sII', header)\n        if magic != b'glTF' or version != 2 or size != final.st_size:\n            raise ValueError(f'Invalid GLB magic, version or declared size: {path}')\n    return dict(path=str(path), bytes=final.st_size, sha256=digest.hexdigest())\n\n\ndef _load(path: Path) -> dict[str, Any]:\n    _fingerprint(path)\n    try:\n        data = json.loads(path.read_text(encoding='utf-8'))\n    except (ValueError, OSError) as exc:\n        raise ValueError(f'Cannot read JSON object: {path}') from exc\n    if not isinstance(data, dict):\n        raise ValueError('Expected a JSON object')\n    return data","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/assets.py#L49-L85","documentation":"_fingerprint takes four stat snapshots (before open, after open, after read, after close) and requires the identity tuple (st_dev, st_ino, st_size, st_mtime_ns, st_ctime_ns) to be identical across all of them. If the file was modified, replaced, truncated, or re-created while being hashed, it raises this error so the digest is never trusted for a moving target.","triggerScenarios":"The asset file is written, truncated, renamed-over, or re-created by another process between the first stat and the final stat during a _fingerprint call.","commonSituations":"A build/download step still writing the asset when ingestion starts; two pipeline jobs ingesting and regenerating the same asset directory concurrently; editors or sync tools (Dropbox, rsync) touching files mid-run.","solutions":["Ensure all asset-producing steps complete before running ingest_assets (pipeline ordering/dependencies).","Use a file lock or atomic rename (write to temp, then os.replace) in the producer.","Re-run the ingest after the writer finishes; the error is transient by nature.","Run only one ingestion job per asset directory at a time."],"exampleFix":"# before\nsubprocess.Popen(['download.sh'])\ningest_assets(manifest)  # races with download\n\n# after\nsubprocess.run(['download.sh'], check=True)\ningest_assets(manifest)","handlingStrategy":"retry","validationCode":"# ensure no writer holds the file: lsof <path> returns empty\nimport subprocess\nbusy = subprocess.run(['lsof', path], capture_output=True).stdout","typeGuard":null,"tryCatchPattern":"import time\nfor attempt in range(3):\n    try:\n        return ingest_assets(manifest)\n    except ValueError as e:\n        if 'File changed while hashing' not in str(e):\n            raise\n        time.sleep(2 ** attempt)","preventionTips":["Producers must write atomically (temp file + os.replace)","Sequence pipeline steps: build finishes before ingest starts","Avoid file-sync tools operating on the asset directory during runs"],"tags":["concurrency","filesystem","toca"],"backgroundTag":"checksum-mismatch","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}