Implement deterministic full-text index generation #70
No reviewers
Labels
No labels
area/deployment
area/governance
area/metadata
area/pipeline
area/search
type/data
type/design
type/implementation
type/testing
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Arkive/arkive!70
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/issue-22-index-builder"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
arkive.index_build, installed asarkive-build-index, which builds the SQLite FTS5 lexical index and its Issue #21 manifest for the bundled core from validated records. The command takes an explicit--scope(v0.1:bundled_coreonly) and--outputdirectory outsidedata/; the domain is never inferred.environment(FTS5 compiled in, no fallback),records(the canonical validator must accept the whole tree, so schema-invalid, cross-record-invalid, anduser_local-under-data/trees are refused),inputs(documented eligibility rules, duplicate chunk identifiers and duplicate insertion keys refused),integrity(every included document's bytes match its recorded checksum and size, every chunk's text equals its[char_start, char_end)slice; otherwise the inputs are stale and refused),build(one table, one transaction, total insertion order, no SQLite side files),manifest(every digest througharkive.index_manifest),verify(the canonical validator checks the staged manifest against the records),commit(atomic per-file rename with rollback of both previous files).inputs.excludedwith a validator-checked reason. Excluding a source covers its documents; including a document includes all its chunks per the v0.1 membership contract. The rules are not configurable.provenance. The builder reproduces the committed fixture artifact byte for byte; the suite asserts this when the SQLite version matches and skips otherwise.pilot-corefixture is now produced by the builder.make_pilot_core_fixture.pyonly fixes the arguments, so the fixture can no longer drift from production output. The committed artifact is unchanged; onlybuild.builder.tooland the platform string in the manifest changed.docs/index-build.md(command, stages, eligibility table, build configuration, determinism claims and non-claims, output handling, relationship to #23/#25/#48), a command section indocs/development.md,docs/index-manifest.mdupdated where it said building was future work, and a CHANGELOG entry.Design notes
validate_treeover the record root and refuses on any problem before loading anything. It has no rule of its own about what a valid record is, so it cannot accept what the validator rejects and cannot drift from it.data/and must not contain the record tree, so a generated index cannot be read back as reviewed content or committed by accident. The repository root itself is therefore refused as an output;build/indexis the documented location and is already gitignored.docs/index-build.md§6.Verification
git diff --checkpython pipeline/validate/validate_records.pyandpython pipeline/validate/validate_records.py data/sources tests/fixtures/index/pilot-corepython -m unittest discover -s tests -v— 427 tests pass (49 new)python -m ruff check src pipeline testspython -m ruff format --check src pipeline testssrc/arkive/index_build.pyafter adding tests for the reachable error paths; the remaining uncovered lines are defensive branches unreachable with a validated tree (unreadable JSON after the validator parsed it, a non-sha256algorithm the schema forbids, an out-of-scope document underdata/, a pre-existing file in a freshmkdtempdirectory, asqlite3.Errorduring the write, and rollback errors that need a failing disk).Scope confirmation
user_localbuildLinked issue
Closes #22
🤖 Generated with Claude Code
The implementation is very solid overall and covers Issue #22 thoroughly: deterministic FTS5 generation, canonical record validation, eligibility/exclusion handling, stale-byte and chunk-offset checks, manifest generation/verification, rollback, reproducibility tests, and network-disabled execution all look well designed.
I found one storage-boundary issue that I think should be fixed before merge:
1. Symlinked parent directories can bypass the path boundary
_check_output_dir()usesabspath()and rejects the final output path if it is itself a symlink, but it does not resolve symlinks in parent components.For example, if:
/tmp/build/link -> <repo>/datathen an output such as:
/tmp/build/link/indexis not itself a symlink and can pass the current lexical checks, while
mkdir()would actually create/write the index under<repo>/data.There is a similar issue in
_document_file(): the lexicalrelative_to(records_root)check pluspath.is_symlink()protects against absolute/..traversal and a symlinked final file, but a parent directory inside the record tree could itself be a symlink resolving outside the tree.Please make these boundary checks operate on the effective/resolved filesystem location (or explicitly reject symlinked path components) and add regression tests for:
data/;data/sourcesis a symlink outside the record tree.2. Please also handle pre-existing SQLite side files
The implementation states that
index.sqlite-wal,index.sqlite-shm, andindex.sqlite-journalmust never remain in a finished index directory, but the current side-file check only checks the staging database. A pre-existing side file in the destination appears able to survive a successful rebuild.Please either fail closed on these files or handle their cleanup safely, and add a rebuild regression test proving the final output cannot retain stale SQLite sidecars.
Everything else I reviewed looks aligned with Issue #22 and the existing manifest/storage contracts. Once these boundary cases are covered, this should be in very good shape for merge.
Thanks, both fixed in
91d68fb:_check_output_dir()now checks the path as given and the symlink-resolved path, for both the output anddata/, and returns the resolved one so writes go where the check looked. A link that resolves somewhere safe (e.g./tmpon macOS) is still accepted._document_file()walks every component below the repository root and refuses any symlink. Regression tests cover both of your cases, plus a symlinkeddata/sources, adata/that is itself a link, and a parent that is a symlink containing the record tree.index.sqlite-{journal,wal,shm}(including a dangling symlink) is refused before the build and again at commit. Arkive never creates these files (the builder writes only in staging, and the index uses the rollback journal), so one means something else wrote to the index. I chose refusing over cleanup because there's no reliable way to tell whether that process is still running: an idle SQLite connection holds no lock. There is a rebuild test for each suffix and one for a side file appearing mid-build.Each new guard was mutation-checked: 9 mutations, all caught; 437 tests pass.
One thing to flag, not fixed here: the validator's core-only rules (symlink refusal,
user_local-under-data/) key on its ownDATA_DIR, so they don't apply when--repository-rootnames another checkout. That's how the document-symlink case actually got through. Eligibility still keepsuser_localrecords out of the index, but it might be worth a follow-up issue to pass the core root intovalidate_tree.