fix(579): single authority for the first-stage online envelope + identity#798
Conversation
…e_guid column must not hijack identity
… identity to batch always-derive Every first-stage record's wrap+identity now flows through one _envelope_row: the payload lands under content.source and source_guid is derived over the RAW payload. Online branches (text/json/csv/tsv/xlsx) produce raw rows and route through _wrap_online_rows; batch _add_batch_metadata and _prepare_text_chunks_batch decorate that same envelope with ancestry. This closes the online tabular/xlsx gap (rows were flat and unstamped, failing at write_source) by construction and drops the online-json honoring of a payload source_guid field, which let a user column named source_guid hijack the framework identity. Batch output is unchanged. XML deferred to 589.
Code review (max effort — 10 finder angles, 1-vote verify, gap sweep)Ran the full pipeline: 10 parallel review angles (5 correctness + 3 cleanup + altitude + CLAUDE.md conventions), each candidate independently re-verified by a fresh skeptical reviewer, then a gap sweep for anything the first pass missed. 5 candidates were checked and cleanly refuted (dropped The core fix (single-authority envelope construction, closing the reserved-field collision) is sound and well-tested. The main risk: 1. Online
|
…keys, and tab-delimited .tsv
…ound 2) Address correctness findings from the PR review: - generate_content_hash: canonicalize dict keys to str + json.dumps(default=str), so a spreadsheet date cell, a ragged csv row's None restkey, or a numeric header hashes deterministically instead of crashing. No-op for JSON-native string-keyed payloads (golden identities unchanged). - TabularLoader: route the csv delimiter on the .tsv extension so tab-separated files parse into real columns instead of one mangled column (was: online .tsv persisted garbage under a valid-looking guid; batch .tsv fixed too). - _wrap_online_rows return type is list[Any] (a non-dict passthrough item can remain). - _prepare_text_chunks_batch delegates to _add_batch_metadata (batch's single row authority); collapse the data_chunk/src_text aliasing in the online branches.
…#798 review) _canonicalize stringified every dict key with str(k), which collapsed two keys that stringify identically (int 2024 vs str '2024'; a None restkey vs a literal 'None' column) into one — a silent source_guid/dedup collision. Keep string keys verbatim (golden identities unchanged) and encode a non-string key as '\x00' + repr(k), which sort_keys can order and which cannot merge with a same-looking string key.
…ambiguous identity)
…798 review) Encoding non-string dict keys for hashing kept re-introducing collisions: str(k) collapsed distinct keys, and a \x00-prefix sentinel still overlapped the string-key namespace. A non-string field name (a numeric spreadsheet header, a ragged csv row's None restkey) is unusable downstream (it becomes a content.source key referenced by name) and has no stable identity, so generate_content_hash now rejects it with a clear DataValidationError. default=str still renders non-JSON-native VALUES (a date cell), which never fires for JSON-native payloads — golden identities unchanged.
Review addressed (verify-first on each finding, then fixed at root cause)Pushed follow-up commits. Full suite 8130 pass, ruff + mypy clean; each fix went RED→GREEN and the identity-function change was re-verified by three independent blind reviewers (the first two caught real collision bugs in my initial attempts — see #4).
Note on the final reviewer's out-of-scope observation: Smoke: skipped — the smoke project isn't provisioned in this clone (branch-independent), and the touched surfaces ( |
Summary
The first-stage envelope (payload →
content.source+ a derivedsource_guid) was authored four times — batch rows (_add_batch_metadata), batch text (_prepare_text_chunks_batch), online rows (json, inline), online text (inline) — and maintained by convention. The copies had already drifted, which is the root cause behind two observable bugs:.csv/.tsv/.xlsxshipped flat, unstamped rows (data_chunk = <loader output>). Post-573 a row withoutsource_guidfails loud atwrite_source, so online tabular workflows were broken at the storage boundary..jsonlet a user column literally namedsource_guidhijack the framework identity (item.get("source_guid") or derive). Sincesource_guidis not a reserved staging name, a row{"source_guid": "x", ...}adopted"x"as its identity, collapsing distinct rows — the reserved-field collision 577 fixed, which the batch path never had.Fix (enforcement, not another patched branch): one authority
_envelope_row(payload)computes{content: {source: {**payload}}, source_guid: derive_source_guid(payload)}for every first-stage record. Online branches now only produce raw payloads and route through_wrap_online_rows; batch's two stamping sites are folded onto the same_envelope_row(behavior-preserving) and keep decorating it with ancestry. A new online file type can no longer regress to flat/unstamped by construction.Task 1 decision (per spec) — always-derive, drop the honoring
Dropped the online-json
item.get("source_guid") orso identity is always derived over the raw payload, matching batch. Verified no caller depends on the honoring: it was introduced in the 584 commit (not a resume feature), no loader stamps a first-stage row before ingestion, and no test asserts it. This both reconciles the batch↔online drift and closes the 577 collision on the online path.Deviations / scope notes
XmlLoader.processreturnsET.Element, not rows, so XML has nothing to wrap on either path — a distinct fix needing a fixture + row-shape decision. Online XML is left as-is (still loud-broken) and tracked by 589._add_batch_metadata/_prepare_text_chunks_batch— byte-identical output (samederive_source_guidcall), guarded green by the existing golden-guid tests (865e85c1…,cac9d8de…).TabularLoaderusescsv.DictReaderwith the default comma delimiter, so.tsvcurrently parses as one column — a separate defect, not envelope-related._wrap_online_rowscorrected after review (it over-specified downstream storage behavior).Verification
test_online_row_envelope_chokepoint.py(7 tests) pinned the flat/unstamped csv+xlsx failures and the jsonsource_guid-hijack collision; all pass after the fix.ruff format/checkclean;mypyclean..csvrow, the same row via online.json, and via batch_add_batch_metadataproduce the samecontent.sourceand the samesource_guid; re-ingesting a.csvyields identical non-blankuuid5guids. Reserved-named user columns (node_id,source_guid) survive undercontent.sourceand no longer hijack identity.risk.sh: LOW, no smoke surface touched (change isinput/preprocessing/staging/, notinput/loaders/orstorage/). The "online tabular reacheswrite_source" claim is proven by composition: rows now carry a non-blankuuid5source_guid, andwrite_sourceraisesDataValidationErroriff the guid is blank. The registry canary is orthogonal (UDF/reprompt discovery, untouched); online smoke-test hangs on the external LLM (environmental).