fix(artifact): reconcile imported hashes against git on bootstrap (1/3)#868
fix(artifact): reconcile imported hashes against git on bootstrap (1/3)#868moofone wants to merge 4 commits into
Conversation
Artifact bootstrap byte-copies a teammate's DB into the local cache, but the imported file_hashes rows carry the exporter's mtime_ns. A fresh clone stamps every file with checkout time, so classify_files (mtime_ns + size only) marks ~every file changed and the first incremental run re-parses the whole repo -- the exact run artifact bootstrap exists to make cheap. Add cbm_artifact_reconcile_hashes(), called from try_artifact_bootstrap right after a successful cbm_artifact_import. It re-stamps the hash rows of files git reports unchanged between the artifact's commit and the local working tree with local stat() values, so the existing, untouched classify_files logic then classifies correctly. Zero changes to the incremental pipeline itself. Trust gate (so a stale/corrupt artifact can never mark a genuinely changed file unchanged -> graph corruption): cbm_artifact_export now writes an optional "reconcile_basis":"git-clean-head" marker into the existing artifact.json ONLY when it can prove the DB matches a clean checked-out tree at `commit` -- commit is a validated hex OID, the working tree has no changes outside .codebase-memory, and every file_hashes row's on-disk mtime+size matches. Reconciliation requires the marker and skips (returns -1) on any doubt: no git, untrusted/missing marker, non-hex/unknown commit, shallow clone, or any popen or parse uncertainty. A skip leaves rows foreign and falls back to today's slow-safe full incremental. No schema_version bump (older binaries ignore the new optional field). Security: commit is hex-validated before command construction and repo_path is shell-validated via cbm_validate_shell_arg (same pattern as git_context.c / watcher.c); git output is parsed as NUL-delimited (-z) directly, never via line-oriented parsing, so paths with newlines/quotes are handled. New popen call site added to scripts/security-allowlist.txt. Reuses two pieces of existing dead/unused code: cbm_artifact_commit() (was never called in production) and cbm_store_upsert_file_hash_batch() (existed but was never called in production -- reconciliation persists all restamped rows in one transaction). Tests (tests/test_artifact.c, following the existing suite's structure): export sets the marker on a clean tree and drops it when dirty; reconcile restamps unchanged rows and leaves changed rows foreign; reconcile skips (-1, rows untouched) on untrusted metadata, unknown commit, and no-git, plus null-arg safety. Signed-off-by: Greg Tiller <tiller@dal.ca>
|
Thanks for splitting this out from #867. Triage: performance/correctness bug, review first in the #868 -> #869 -> #870 stack. Because this touches artifact import and the security allowlist file scripts/security-allowlist.txt, review will be a bit cautious: git trust boundaries, path handling, unchanged-file detection, and the necessity/scope of the allowlist update all need to be checked. The focused test coverage in this PR is the right shape. |
git diff/ls-files are blind to gitignored files, but a gitignored file can still be indexed (.cbmignore negation un-skipping a generated dir, DeusData#500) and thus carry a file_hashes row. Without a tracked-set gate such a row would be restamped as "unchanged" even though git cannot vouch for its content, leaving stale graph data after bootstrap. Reconciliation now also captures git ls-tree -r -z --name-only <commit> and restamps only rows tracked at the artifact commit AND absent from the changed set; everything git cannot vouch for stays foreign and is re-parsed. Same validation funnel (hex-validated commit, shell-validated repo path, NUL- delimited parse with truncation guard); allowlist entry text extended to mention ls-tree. Adds regression test artifact_reconcile_skips_untracked_rows. Refs DeusData#867 Signed-off-by: Greg Tiller <tiller@dal.ca>
|
Thanks for the triage focus areas — here's a map of each to the code, plus one hardening commit pushed in response. New commit Concern → where it's handled: Git trust boundaries
Path handling
Unchanged-file detection
Allowlist necessity/scope
Also filed #885 for the cross-path bootstrap limitation noted in #867's deferred list (pre-existing, caps this PR's team-sharing benefit until fixed). Full ASan/UBSan suite green with the new commit. Happy to adjust the trust-marker convention if you'd prefer a different shape for point (a). |
CI lint (clang-format-20 --dry-run --Werror) flagged comment alignment and line-break style in the P1 additions. Formatting only; no code changes. Refs DeusData#867 Signed-off-by: Greg Tiller <tiller@dal.ca>
What & why
After artifact bootstrap,
try_artifact_bootstrapbyte-copies a teammate's DB into the local cache, but the importedfile_hashesrows carry the exporter'smtime_ns. A fresh clone stamps every file with checkout time, soclassify_files(mtime+size only) marks ~every file changed and the first incremental run re-parses the whole repo — the exact run artifact bootstrap exists to make cheap.Change
Add
cbm_artifact_reconcile_hashes()insrc/pipeline/artifact.c(+artifact.h), called fromtry_artifact_bootstrapimmediately after a successfulcbm_artifact_import. It re-stamps thefile_hashesrows of files git reports unchanged between the artifact's commit and the local working tree, using localstat()values. The existing, untouchedclassify_fileslogic then classifies correctly. Zero changes to the incremental pipeline itself.Reuses two pieces of existing dead code:
cbm_artifact_commit()(never called in production) andcbm_store_upsert_file_hash_batch()(existed, never called in production — reconciliation persists all restamped rows in one transaction).Trust gate (the design point flagged in #867)
cbm_artifact_exportnow writes an optional"reconcile_basis":"git-clean-head"field to the existingartifact.json, only when export can prove the DB matches a clean checked-out tree atcommit:commitis a valid hex OID (40/64-char) fromgit rev-parse HEAD;.codebase-memory/(:(exclude)pathspec);file_hashesrow's on-diskmtime_ns + sizematches the stored stamp (belt-and-suspenders that catches a stale/swapped DB even when the tree looks clean).Reconciliation requires the marker and the commit to be present + hex-valid + resolvable locally, and skips (returns -1) on any doubt: no git, untrusted/missing marker, non-hex/unknown commit, shallow clone, or any popen/parse uncertainty. A skip leaves rows foreign → today's slow-safe full incremental. No
schema_versionbump (older binaries ignore the unknown field).Security
commitis hex-validated before any command construction;repo_pathis shell-validated viacbm_validate_shell_arg(same pattern asgit_context.c/watcher.c); git output is parsed as NUL-delimited (-z) directly (never line-oriented), so paths with newlines/quotes are handled. A non-empty-zbuffer not ending in NUL is treated as corrupt → skip. Newcbm_popencall site added toscripts/security-allowlist.txtwith justification.Tests (
tests/test_artifact.c, following the existing suite)artifact_export_marks_clean_basis— marker set on a clean export, dropped when the tree is dirty.artifact_reconcile_restamps_unchanged— full-index → export → clone → modify 2 + add 1 → import → reconcile restamps unchanged rows to local mtime, leaves changed rows foreign (restamped == 3).artifact_reconcile_skips_untrusted_metadata— strip the marker →-1, rows untouched (regression guard for old/manual/dirty/stale artifacts).artifact_reconcile_skips_unknown_commit— valid-hex-but-absent commit →-1.artifact_reconcile_skips_without_git— no.git→-1.cbm_artifact_reconcile_hashes.All fixtures generated at runtime in tmpdirs; no committed fixtures.
scripts/test.sh(ASan+UBSan) green locally — full suite passes.Benchmark (S2 = bootstrap + first incremental, K = 7 changed)
changedchangedCandidate S2 wall is 4.8 % of baseline at N=8000 (~21× faster). After S2, candidate node/edge counts exactly equal a from-scratch full index of the same tree (500/2000/8000). Full matrix + raw CSV kept outside the repo.
Checklist
scripts/test.sh(ASan/UBSan) green.popen/cbm_popensite justified + added toscripts/security-allowlist.txt.scripts/lint.sh(clang-tidy/cppcheck/clang-format) — not installed locally; verified ≤100-col + style by hand, CI is the gate.Seeking maintainer sign-off on the
artifact.jsontrust-marker reuse (point (a) in #867). Happy to rework if a different convention is preferred.