feat(operators): add GroupBy for many→one reduction, with logical-type and provenance fixes (NPIPE-204) - #250
Conversation
f73f539 to
b93ab32
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
Adds a first-class many→one reduction operator (GroupBy) to the operator layer, along with underlying provenance + logical-type aggregation fixes needed for job.run() correctness and cache stability.
Changes:
- Introduces
GroupBy(by=[...])and registers it for serialization and the stream fluent API. - Fixes list-valued provenance handling by widening
SourceInfoValueand deriving_source_*Arrow/Python types from stored values. - Adds shared aggregation helpers (
fold_system_tag_values,build_aggregated_table) and updatesBatchto fold system tags to scalar digests while preserving list-valued member/source columns; adds job-level regression coverage.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/test_utils/test_arrow_utils.py | Adds pinned + cross-process tests for system-tag folding digests. |
| tests/test_pipeline/test_aggregation_job.py | Adds job-level Delta-backed aggregation tests covering memoization + MergeJoin regression. |
| tests/test_core/operators/test_operators.py | Extends Batch tests to assert scalar system tags and list-valued source columns. |
| tests/test_core/operators/test_group_by.py | Adds comprehensive operator-level tests for GroupBy shape, ordering, validation, schema prediction, and logical types. |
| tests/test_core/datagrams/test_data_source_info_types.py | Adds unit tests for list-valued _source_* provenance typing/round-tripping in Data. |
| test-objective/integration/test_provenance.py | Updates objective provenance assertions for “reducing” ops (system tags scalar; user cols list-wrapped). |
| superpowers/specs/2026-08-07-npipe-204-batch-group-by-design.md | Adds design spec documenting API deviation and implementation details. |
| superpowers/plans/2026-08-07-npipe-204-groupby-operator.md | Adds implementation plan / checklist for the work. |
| src/orcapod/utils/polars_data_utils.py | Removes dead add_source_info implementation. |
| src/orcapod/utils/arrow_utils.py | Adds fold_system_tag_values and build_aggregated_table to support many→one ops + logical-type-safe list aggregation. |
| src/orcapod/types.py | Introduces SourceInfoValue type alias for recursive list-valued provenance tokens. |
| src/orcapod/protocols/core_protocols/datagrams.py | Updates DataProtocol source-info typing to SourceInfoValue. |
| src/orcapod/pipeline/serialization.py | Registers GroupBy in the operator registry for (de)serialization. |
| src/orcapod/core/streams/base.py | Adds .group_by(...) fluent method to StreamBase. |
| src/orcapod/core/streams/arrow_table_stream.py | Widens source_info typing to SourceInfoValue. |
| src/orcapod/core/operators/group_by.py | Implements the new GroupBy operator (partition, deterministic ordering, system-tag folding, schema prediction). |
| src/orcapod/core/operators/batch.py | Updates Batch aggregation semantics to fold system tags scalar + use shared aggregated-table builder. |
| src/orcapod/core/operators/init.py | Exports GroupBy from the operators package. |
| src/orcapod/core/datagrams/tag_data.py | Makes Data derive _source_* Arrow/Python types from stored provenance values (including recursive lists). |
| DESIGN_ISSUES.md | Logs/updates operator-layer design issues and marks the tag_data.py half of U1 as resolved. |
| CLAUDE.md | Updates architecture guidance for reducing ops, GroupBy, and aggregated logical types. |
| .zed/rules | Mirrors the same documentation updates for Zed AI rules. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| import subprocess | ||
| import sys | ||
|
|
||
| script = ( | ||
| "from orcapod.utils.arrow_utils import fold_system_tag_values\n" | ||
| "rids = [bytes.fromhex('0102030405060708090a0b0c0d0e0f10'),\n" | ||
| " bytes.fromhex('1112131415161718191a1b1c1d1e1f20')]\n" | ||
| "print(fold_system_tag_values('_tag_record_id::abc123', rids).hex())\n" | ||
| "print(fold_system_tag_values('_tag_source_id::abc123', ['src_a','src_b']))\n" | ||
| ) | ||
| out = subprocess.run( | ||
| [sys.executable, "-c", script], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True, | ||
| ).stdout.split() |
There was a problem hiding this comment.
Fixed in 3ce1be6 — explicit PYTHONPATH, prepended so an existing value survives.
On severity: it was not failing. The venv has an editable install whose .pth points at src, so sys.executable resolves orcapod regardless of cwd — verified from /tmp with PYTHONPATH unset, and CI was green on 3.11/3.12. Still right in principle, since the test depended on install mode rather than the suite path config.
…IPE-204) Adds the design for NPIPE-204: `group_by` on the `Batch` operator for many->one reduction, plus the type-aware source-info fix that unblocks it. Also expands DESIGN_ISSUES U1 to record the `tag_data.py` half of the hard-coded `large_string` source-info type, which is the actual cause of the `ArrowTypeError` that breaks both `Batch` and `MergeJoin` inside `job.run()`. NPIPE-204 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Splits the reduction semantics out of `Batch` into a new `GroupBy`
operator, so each class keeps one output contract and `Batch`'s streaming
async path stays untouched. This deviates from NPIPE-204's stated
`Batch(group_by=...)` API; the issue and the downstream wiring both need
updating.
Also from review:
- Pin the system-tag fold to `combine_hashes` and `uuid.uuid5`, reusing
the convention in `stream_builder._make_record_id`, and add a subprocess
test. Nothing previously guaranteed cross-process digest stability.
- Drop the `_context_key` row from the column table. Verified that
`as_table(columns={"source", "system_tags"})` excludes it, so no
operator ever sees a context key -- logged as DESIGN_ISSUES O2 instead.
- Record the dead `polars_data_utils.add_source_info` as a third
hard-coded site under U1; delete it rather than fix it.
- Require >=2 groups in the invalidation fixture, and cover empty input.
NPIPE-204
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ten TDD tasks: type-aware source info in Data, delete the dead polars add_source_info, the shared system-tag fold helper, Batch's provenance fix, the GroupBy operator, schema-mirroring and registration, job-level tests, and docs. NPIPE-204 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_ensure_source_info_table and Data.schema hard-coded large_string / str for every _source_* field, so any operator producing a list-valued provenance token crashed with ArrowTypeError inside job.run(). MergeJoin already does this (merge_join.py:262) and was silently broken. Types are now derived from the stored value. None still maps to large_string, so unknown-provenance columns are unchanged. No pipeline-DB schema bump: a node's source-column type is fixed by its own output schema. Refs DESIGN_ISSUES U1. NPIPE-204 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tions (NPIPE-204) Review follow-up to 1b570e6. - Move SourceInfoValue from core/datagrams/tag_data.py to types.py, next to TagValue/DataValue, so protocols/ can reference it without inverting the protocols -> core layering. Not exported from core/datagrams/__init__.py; types.py is the visibility surface. - Cover the recursive descent in _source_info_arrow_type / _source_info_python_type with a nested-token case. The previous tests all passed against a non-recursive implementation. - Widen DataProtocol.source_info / with_source_info and ArrowTableStream.__init__ from str | None to SourceInfoValue. The narrow annotation is already false at runtime: _materialize_to_stream and sync_orchestrator pass a MergeJoin's list-valued tokens straight into that parameter. EmptyData.empty_source_info stays str | None -- nothing in src/ constructs it, so it cannot reach _ensure_source_info_table. - Annotate _source_info_python_type as -> DataType; list[str] is a GenericAlias, not a type, and the value flows into Schema. - Narrow both isinstance checks to list, matching the alias; no code path produces a tuple. NPIPE-204 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zero callers in src/, no test coverage, and a latent shadowing bug where source_column is rebound to a pl.Series inside the per-column loop. It was also a third site hard-coding a scalar string type for source info. Refs DESIGN_ISSUES U1. NPIPE-204
…rs (NPIPE-204) System tags must stay scalar because _build_record_id_preimage hashes them directly. Each column folds independently over its ordered member values: record_id via uuid5 (matching stream_builder._make_record_id), source_id via combine_hashes. Both SHA-based, so the digest is stable across processes -- pinned by a subprocess test, since a hash()-based fold would look correct within one process and miss the cache on every new driver run. NPIPE-204 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…E-204)
Batch list-wrapped every column including the system tags, but record
identity hashes those columns directly, so they must stay scalar. They now
fold via fold_system_tag_values and gain a ::{pipeline_hash} name suffix,
mirroring the name-extending rule joins already use.
Partitioning, list-valued tag columns, and the streaming async_execute path
are unchanged.
NPIPE-204
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…PE-204) Every other operator preserves one row per tag; GroupBy collapses N rows sharing a tag tuple into one packet with list-valued members, which is what lets a downstream pod receive a whole recording session at once. Group keys stay scalar tags; non-key tags are promoted to list-valued data columns rather than dropped, so consumers can tell which member each element came from. Members sort by non-key tag values so the hashed lists are stable across runs. NPIPE-204
- unary_output_schema no longer predicts _source_* columns for promoted tags.
ArrowTableStream.output_schema returns the data schema unconditionally, so
columns.source is a no-op at the schema level; the old prediction was also
internally inconsistent, declaring _source_probe but not _source_path.
- record_id is now an unconditional final tiebreaker in the member sort rather
than an elif fallback. The elif branch was dead whenever any non-key tag
existed, leaving duplicate tag tuples ordered by emission order, which Ray
scheduling and DB fetch order make nondeterministic.
- Reject list-valued group keys in validate_unary_input. Batch list-wraps its
tag columns, so Batch -> GroupBy previously died with a bare
TypeError: unhashable type: 'list' naming neither operator nor column.
- Reject duplicate names in by, which produced a duplicated output tag column.
- Add output-schema parity tests across the four {source, system_tags} configs,
comparing against the materialized stream from unary_static_process rather
than process(), whose DynamicPodStream delegates back to the pod and would
make the comparison circular.
- Tighten the record_id ordering test to assert the exact emitted list; it
previously sorted the output first, making it blind to ordering.
- Drop unused logging import and function-local ArrowTableStream import.
NPIPE-204
…IPE-204) Without the registry entry a pipeline containing a GroupBy cannot be deserialized. NPIPE-204 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…PE-204) Every existing aggregating-operator test stopped at op.process() + as_table(), which never reaches _materialize_to_stream -- the reason the list-valued provenance crash shipped. These run the full job.run() path against a Delta Lake store and assert memoization holds across identical runs and invalidates only the group whose member changed. NPIPE-204 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
System tag evolution rule 3 described Batch as purely type-evolving (str -> list[str]), which was only ever true of user tag and data columns -- system tags fold to a scalar and extend their name. Renames the rule to "reducing" and covers both Batch and GroupBy, including why the fold must be SHA-based rather than hash(). Also records two schema-verification traps found during implementation: a prediction must be checked against unary_static_process(...).output_schema(), not against process() (circular) or as_table() (legitimately differs). DESIGN_ISSUES: - U1 tag_data.py half resolved; arrow_utils.py half stays open. - O3 (new): _materialize_to_stream applies row 0's source_info to every row, so rows 1+ are mis-attributed. Pre-existing and equally wrong for scalars, but the U1 fix turns what was a loud crash into a quiet wrong answer. - O4 (new): GroupBy member order is undefined when the input has duplicate tag tuples. Not fixable inside the operator -- the root cause is that DuplicateTagError is defined but never raised, so duplicates are never prevented. NPIPE-204 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…IPE-204) Batch and GroupBy built their output schema with pa.list_(field.type), which raises ArrowNotImplementedError on any extension-typed column: Arrow cannot embed an extension type inside a list value field (DESIGN_ISSUES ET1/ET2). A pod annotated -> Path emits extension<orcapod.path>, so grouping the column the common-clock pipeline actually needs was impossible. ITL-173 (#251) fixed the type-declaration layer with ListLogicalType but never touched the operator layer, so declaring list[Path] worked while grouping it still failed. Both operators now build through arrow_utils.build_aggregated_table, which constructs the list over the element's storage type and wraps it in the outer list[<element>] extension type. Verified end to end: sync pod -> Path, GroupBy, downstream pod annotated list[Path] receives real Path objects through job.run(). The Path -> str conversion pod callers were using as a workaround is no longer needed. Adds a path-typed regression fixture. Every previous fixture used plain large_string/int64, which is why this shipped uncaught. The new fixture is defined at module level on purpose: this file uses `from __future__ import annotations`, and a pod nested in a test method has unresolvable stringified annotations. Also updates the ET2 section's stale src/orcapod/extension_types/ path after main's rename to logical_types/. NPIPE-204 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rule (NPIPE-204)
Two tests in test-objective/ encoded the old rule that Batch list-wraps system
tag columns. That behaviour changed deliberately: system tags must stay scalar
because _build_record_id_preimage hashes them directly to derive record
identity. They now fold to a digest and gain a ::{pipeline_hash} name block.
The tests are the thing that was wrong, not the code. Updated to assert both
halves of the rule, which makes them stronger than before:
- user tag and data columns DO become lists
- system tag columns stay scalar AND gain a name-extension block
Renames TestTypeEvolving -> TestReducing to match the rule's new name in
CLAUDE.md, and splits the single batch test so a failure says which half broke.
These were missed locally because test-objective/ is a separate suite with its
own workflow (run-objective-tests.yml) and is not covered by `pytest tests/`.
NPIPE-204
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… (NPIPE-204) After an upstream pod's output changes, the first job.run() emits only the rows recomputed in that pass. For a per-row pod that emission is complete; for a many->one operator it is partial — the group is reduced over only the members present, the reducing pod executes, and its result is persisted with nothing marking it incomplete. It self-corrects on the next run. Measured with two upstream pod stages: the per-row control converges in one run while the grouped path needs two, executing on a one-member group first. So the behaviour is NOT identical with and without GroupBy, which is how it was previously characterised. Batch behaves the same way, so this is pre-existing pipeline semantics surfaced by reduction rather than something GroupBy introduced — but it matters more for GroupBy, which exists precisely so a pod can reason over a complete group. Not fixable inside the operator: an operator sees whatever its upstream emits and cannot know whether a group is complete. Logged for reviewer judgment rather than fixed here. NPIPE-204 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
72c88b0 to
f069a98
Compare
…lumn (NPIPE-204) A joined stream carries one record_id system tag per canonical input position. The broadcast side of a fan-out repeats its id on every row it was joined into, so it cannot separate those rows -- only the fanned-out side's record_id can. The tiebreaker took the first record_id column in table order, so whenever the broadcast input sorted to canonical position 0 the sort key tied on exactly the rows the tiebreaker exists for, and members fell back to emission order. DB fetch order and Ray scheduling make that nondeterministic, so a reducing pod's input hash could change between runs and trigger a spurious recompute. Append every record_id column, sorted by name so the key order does not depend on column order in the input table. Reachable only when a group has two or more members with identical non-key tags -- otherwise a non-key tag already breaks the tie -- and only when the broadcast input sorts first canonically. The new test pins both conditions; its literals are load-bearing, and it fails with [10, 20] != [20, 10] without the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review + one fix pushed (
|
…NPIPE-204) The operator reference pages listed every operator except the one this branch adds, and the hashing reference still described the pre-change Batch behaviour. - docs/api/operators.md: add the GroupBy mkdocstrings entry. - docs/concepts/operators.md: add a GroupBy section (verified against a real run), note the many-to-one exception under the operator/function pod boundary, and mention grouping in the UnaryOperator category. - docs/reference/hashing.md: site 6's "Batch list-wraps system tags" note was made stale by this branch; replace it and add site 6b for the aggregating operator fold in arrow_utils.fold_system_tag_values. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review: consistency with the rest of the codebaseReviewed specifically for whether this matches the conventions and rigor of the surrounding code. It largely does; one real gap, which I've now fixed on the branch, plus a few small items left for you to judge. Conforms
Gap — fixed in 6f26a20User-facing docs did not include the operator.
I pushed 6f26a20 covering all three: the mkdocstrings entry, a Minor, left to your judgment
Not a regressionpyright reports 2 errors in On the 🤖 Generated with Claude Code |
|
The PR looks amazing and implemented extremely well and aligned to the rest of the codebase --- thank you! I just had a few notes mostly asking to put TODO and/or create follow up issues |
Review asked for TODOs and deferred design questions to be marked rather than resolved in this PR. - tag_data.py: TODO on _source_info_arrow_type / _source_info_python_type. Both only understand scalars and list[T]; structured source-info values need their own branches, and the recursion should dispatch on a logical type rather than isinstance(value, list). Deriving the element type from value[0] is also a shortcut -- correct for the homogeneous lists many->one operators produce, not in general. - test_group_by.py: document why member/group ordering is load-bearing (the emitted lists are hashed into the memoization key, so an unstable order causes spurious recomputes, and upstream emission order is not stable across runs), while marking the open question -- which order to use and whether to promise one publicly -- as deferred. Same for empty-group handling: "zero rows in, zero groups out" is the current behaviour but was never designed. Both tests now state that they pin the status quo so a change is deliberate. - test_arrow_utils.py: pass an explicit PYTHONPATH to the cross-process digest subprocess. It does not inherit pytest.ini's `pythonpath = src`, so it was relying on orcapod being installed in the active environment. That holds here (editable install) and in CI, but the test should not depend on install mode. No behaviour changes. NPIPE-204 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two deferred questions from PR #250 review -- GroupBy member/group ordering guarantees and empty-input behaviour -- now have a tracking issue, ITL-630 (Tools / Orcapod Python v0.2 Feature Sprint). It also absorbs DESIGN_ISSUES O4, since duplicate-tag ordering is the same "what do we promise" question. Retargets the notes from NPIPE-204 to ITL-630 and adds real `# TODO(ITL-630)` comment lines alongside the class docstrings, so they are visible to anyone grepping for TODO comments rather than reading docstrings. No behaviour changes. NPIPE-204
…204) ITL-627 Defect 2 reported that list-wrapping a File column lost content hashing: build_aggregated_table produced a list[orcapod.file] hashed by its storage values (JSON path strings), so editing a file silently failed to invalidate. Verified against 8968018, before this branch was rebased. It does not reproduce on the current base -- #257 fixed it in SemanticHashingVisitor.visit_extension, which previously short-circuited for generic aliases like list[File]. That fix added thorough unit coverage in test_extension_type_hashing.py::TestListExtensionHashing, but nothing exercises the job level, which is where the failure was observed and where it fails silently rather than raising. Adds a parametrized per-row/grouped test: cold run executes, identical re-run hits the cache, and a content edit at the same path re-executes. Mutation-checked by disabling the list/set unwrapping branch in visitors.py -- the grouped case fails while per_row still passes, so the test isolates the list-wrapping regression rather than scalar File hashing. NPIPE-204
Summary
Adds
GroupBy, orcapod's first many→one operator, and fixes two bugs found along the way —one already shipped, one that made the new operator unusable on the column type it was built
for.
GroupBy(by=["subject", "date"])collapses N rows sharing a tag tuple into one packet: groupkeys stay scalar tags, every other column becomes list-valued. This is what lets a downstream
pod receive a whole recording session at once — every existing pipeline fans out, and nothing
reduced.
NPIPE-204 specifies
Batch(group_by=[...]). This PR adds a separateGroupBy(by=[...]).Batchexists for throughput and pipelining — its ownasync_executedocstring says batchinglets "downstream consumers start processing before all input is consumed" — and its git history
contains only refactors. Folding reduction into it would give one class two output contracts
selected by which kwarg you passed. Splitting keeps one contract per class, leaves
Batch'sstreaming path untouched, and makes
GroupBy's barrier structural rather than a conditionalinside a streaming operator.
The Linear issue still shows the old API. Full reasoning in
superpowers/specs/2026-08-07-npipe-204-batch-group-by-design.md.Two bugs fixed
1.
Datacould not represent list-valued provenance — already shipped and silently broken._ensure_source_info_tablehard-codedlarge_stringfor every_source_*column, so anyoperator emitting a list-valued token crashed:
MergeJoinalready does this (merge_join.py:262) and was failing insidejob.run()on966d759a. Reproduced independently ofBatch; now covered by a job-level regression test.Resolves the
tag_data.pyhalf ofDESIGN_ISSUES§U1.2. Aggregating operators dropped logical element types.
BatchandGroupBybuilt theirschema with
pa.list_(field.type), which raisesArrowNotImplementedErroron anyextension-typed column — Arrow cannot embed an extension type inside a list value field
(§ET1/ET2). A pod annotated
-> Pathemitsextension<orcapod.path>, so grouping the columnthe motivating consumer actually needs was impossible. ITL-173 (#251) fixed the
type-declaration layer with
ListLogicalTypebut never touched the operator layer, sodeclaring
list[Path]worked while grouping it still failed.Both operators now build through
arrow_utils.build_aggregated_table, which constructs thelist over the element's storage type and wraps it in the outer
list[<element>]extensiontype. Verified end to end:
Downstream pods can take
list[Path]directly — noPath→strconversion pod needed.Design decisions
MergeJoinfor free; keeps per-member provenance readable. No pipeline-DB schema bump — a node's source-column type is fixed by its own output schema._build_record_id_preimagehashes them directly, so they must stay scalar. Folding rather than dropping preserves the Merkle link, so an upstream recompute yielding identical data still invalidates.uuid5+combine_hashes, neverhash()record_idas tiebreakerBatchandGroupByhad the same defect; fixing it once keeps them from drifting.Testing
tests/: 4742 passed, 93 skipped, 2 xfailed, 0 failurestest-objective/: 567 passed, 7 xfailed — note this is a separate suite with its ownworkflow and is not covered by
pytest tests/.New job-level coverage in
tests/test_pipeline/test_aggregation_job.pycloses the gap that letbug 1 ship — every prior aggregating-operator test stopped at
op.process()+as_table(),which never reaches
_materialize_to_stream. Against a realDeltaTableDatabase:MergeJoinregression test for bug 1Two
test-objectiveprovenance tests were updated: they asserted the old rule thatBatchlist-wraps system tags. That changed deliberately, so the tests were wrong, not the code. They
now assert both halves — user columns do list-wrap, system tags stay scalar and gain a
name-extension block.
DESIGN_ISSUES§O5A reduction can persist a result computed from an incomplete member set. After an upstream
change, the first
job.run()emits only the recomputed rows. For a per-row pod that iscomplete; for a reduction it is partial — the group executes and its result is persisted with
nothing marking it incomplete, then self-corrects on the next run.
Batchbehaves identically, so this is pre-existing pipeline semantics surfaced by reduction —but it matters more here, because
GroupByexists so a pod can reason over a complete group.For
common_clock_opan intermediate run can write analignment.jsonbuilt from one of asession's probes.
This is not fixable inside the operator — an operator sees whatever its upstream emits and
cannot know whether a group is complete. A fix needs a completeness signal at the node level.
I'd value a reviewer's judgment on whether this gates the downstream rev bump; that depends on
how
common_clock_op's output is consumed, which the sync-and-qc side knows better than I do.Other known limitations, logged not fixed
_materialize_to_streamapplies row 0'ssource_info()to every row, so rows 1+are mis-attributed. Pre-existing and equally wrong for scalars, but this PR makes it more
visible: the list case previously crashed loudly and now completes with wrong tokens.
GroupBymember order is undefined when the input has duplicate tag tuples. Rootcause is that
DuplicateTagErroris defined but never raised, so duplicates are neverprevented. Does not affect the
common_clock_opwiring, which groups on a strict subset.Now tracked in ITL-630 (below).
_context_key. Pre-existing, layer-wide.§U1'sarrow_utils.add_source_info_to_table()half remains open. The deadpolars_data_utils.add_source_info(a third site, with a latent shadowing bug) was deleted.Follow-up tracked: ITL-630
Two design questions raised in review are deferred rather than settled here, and are tracked in
ITL-630
(Tools / Orcapod Python v0.2 Feature Sprint, alongside ITL-173 and ITL-627):
emitted lists are hashed into the memoization key, and upstream emission order is not stable
across runs. Which order, and whether the operator promises one publicly, is open.
never designed; emit-nothing, emit-empty-group, and raise were not weighed.
ITL-630 also absorbs §O4, since duplicate-tag ordering is the same "what do we promise"
question. §O5 is noted there as related but out of scope — its cause is pipeline propagation,
not operator semantics.
TODO(ITL-630)comments mark both sites intests/test_core/operators/test_group_by.py. Thetests pin current behaviour so a change is deliberate rather than accidental.
The third review note — the source-info type-derivation shortcut in
core/datagrams/tag_data.py— carries its own
TODO(NPIPE-204)and is not part of ITL-630.Usability gotcha worth knowing
A pod function nested inside another function, in a module using
from __future__ import annotations, cannot resolve non-builtin annotations —Patharrives asthe string
'Path'with no resolvable scope. Module-level pods are fine. This bit the new testfixture and would bite any pipeline module written the same way.
Follow-up
Rebased onto current
main, soListLogicalType(#251) andGroupByare in one tree. Mergingrequires a coordinated rev bump in both orcapod-sync-and-qc and orcapod-spikesorting — they
are deliberately kept on the same rev because they target the same Ray cluster.
Relates to NPIPE-204.
🤖 Generated with Claude Code