Skip to content

Commit d599fbe

Browse files
Feature/extend vertex merge vertex router (#350)
* improved vertex merge for vertex router * added manifest metadata * added manifest metadata * make it filter-like
1 parent 723d267 commit d599fbe

57 files changed

Lines changed: 3964 additions & 839 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.12.2]
9+
10+
### Added
11+
12+
- **A marker affix on the value can admit it, instead of merely being trimmed off it.** `graflo.util.transform` gains `affix_gated_key`. Identity alignment already had `gated_normalized_key`, which gates on a *sibling* field and cleans the key with `strip_prefix` — and `strip_prefix` is `str.removeprefix`, a silent no-op when the prefix is absent. So `ABC-Alpha` and a bare `Alpha` normalize to the same key and fuse: the marker carries no authority, and there was no way to say *"this affix is what makes the value a canonical id"*. `affix_gated_key` reads one field and makes the marker the admission test — carried, it is stripped and the value is accepted; absent, the result is `None`. The marker is a `prefix`/`suffix` pair rather than a prefix alone, since which end carries the convention is arbitrary: a marker may lead (`ext_42`), trail (`42-legacy`), or bracket the key, and both halves are required when both are named. Each defaults to `""`, which every string carries, so naming one leaves the other unconstrained and naming neither admits everything while stripping nothing — the convention that lets a source participating unconditionally reuse the same call, so the two normal forms cannot drift. Affixes that overlap on a short value are rejected rather than double-consumed. The returned `None` is a fall-through, not a drop: it is an empty value to identity digests, so the funnel branch listing the attribute is skipped and the record keys on its `local_key`, ingested but outside the cross-source cluster. The marker test stays case-sensitive even under `casefold`, which applies to the surviving key. `gated_normalized_key` is unchanged — it remains the right idiom when a different column decides participation. **Example 21** now demonstrates the marker form, with two `firm` rows carrying the same business name and only the marked one fusing.
13+
14+
- **A manifest can carry its own name and description.** `ManifestMetadata` gains `name` and `description` alongside `provenance`. Until now a `GraphManifest` had no name of its own: the displayed one was smuggled through `Schema.metadata.name`, so a manifest carrying only `bindings` — a legal manifest — was literally unnameable, and anything rendering it fell back to an identifier. Both fields sit outside the content hash for the same reason provenance does: `manifest_hash` covers the three blocks and nothing else, so renaming a manifest cannot move its content address and two routes to the same
15+
a world model still compare equal.
16+
17+
### Fixed
18+
19+
- **`compose_manifests` no longer discards the right side's metadata.** The fold copied the left side's `GraphMetadata` wholesale and adjusted only `name`, so the right side's `description`, `semantics` and `naming` vanished into a composed schema that demonstrably contained the right side's types. Each is now folded on its own terms: descriptions are concatenated in side order (neither side's prose is authoritative); `semantics.exact_match` and `semantics.synonyms` union, while a disagreeing single-valued `semantics.iri` **clears** rather than electing the left side's concept as the composed schema's meaning; and a `NamingConvention` survives only while both sides declare the same one, because `rename_map` is computed from it and asserting the left side's style over names that are not in it would be a false claim. Manifest-level `metadata.name` and `.description` fold the same way.
20+
21+
The same fold also carried the left side's `provenance` into the composed schema, so a composed artifact claimed a content address belonging to one of its inputs. It is now dropped: a compose produces a new artifact, and stamping one is a commit point's job.
22+
23+
24+
25+
### Added
26+
27+
- **Identity alignment works on a routed source, without splitting the router.** A `vertex_router` is how a single heterogeneous stream — an SQL view, one API feed — becomes several classes. When an equivalence collapses two of its branches onto one class, an `IdentityAlignment` on that class used to derive nothing, silently, and every routed record fell out of the graph: derivations were appended at the **root** of the resource pipeline, an actor reads its transform buffer at its own `LocationIndex` with no ancestor fallback, a `descend` subtree runs *before* its own level's transforms, and a transform whose declared inputs are missing skips without recording a failure. The manifest looked right; the emitted graph had no identities.
28+
29+
**Derivations now land at the level that produces the class.** `AddResourceTransformsOp` gained `at` — a per-resource path of `descend` step indices, root by default, so nothing existing moves — and `alignment_to_ops` resolves it from `IdentityAlignment.vertex` through the new `find_vertex_producing_levels`. What it cannot resolve, it refuses: a resource that never produces the class, one producing it at several levels with no `IdentityAlignment.at` override, an `at` that does not address a `descend` level, and — the case the whole resolution exists for — an `at` pointing at a level that produces nothing.
30+
31+
**A resource may derive one attribute several ways.** `AlignmentAttribute.sources[resource]` and `LocalKeySpec.sources[resource]` each accept a list, for when the branches collapsing onto the class carry different key columns; `LocalKeySource` gained `gate` / `gate_prefix` for branches that differ only by discriminator. The lowering is not simply two steps writing the same key: that works on a plain `vertex` step, whose buffer extraction skips `None`, but **not** behind a router, which merges the transform buffer into one observation dict where a later `None` overwrites an earlier real value. So a multi-source attribute becomes one gated step per branch writing a scratch field plus one `coalesce_fields` step (`strategy: all`, so an absent branch column does not take the coalesce down with it) as the single writer. A single spec still lowers to exactly one direct step.
32+
33+
**Delivery through the router is repaired too.** A router builds its child `VertexActor` at `lindex.extend((role, 0))`, where the transform buffer is empty, so derived attributes arrive only through the merged observation — subject to `keep_fields` and `extraction_scope`, which a plain `vertex` step bypasses entirely. New fundamental `EnsureExtractedFieldsOp` widens a restrictive router: `keep_fields` gains the canonical attributes, and under `mapped_only` so does `vertex_from_map[<class>]`, **seeded from the router-level `from`** — creating that entry from scratch would replace the author's projection rather than extend it. Only the aligned class's entry is touched, so the router keeps serving its other types unchanged. A sibling class routed at the same level that already declares one of the canonical attribute names now raises: it would silently absorb the derived value.
34+
35+
Also: `graflo.util.transform` gained `coalesce_fields` and `gated_tagged_key`. **Example 21** (`examples/21-router-union-alignment/`) works it end to end — five records from a nested router and two plain resources collapsing to three vertices, with the unaligned branch still flowing through the same router, unpolluted.
36+
37+
- **`normalize_actor_step` descends into an already-typed `descend`.** A `descend` written in its own typed form could not carry the shorthand sub-steps its `{descend: {...}}` spelling accepts, because `pipeline` is a union discriminated on `type` and the normalizer returned early whenever `type` was present. The same early return left every structural pipeline scan — referenced vertices, renames, level lookup — blind to a `vertex_router` authored in its flat `type_field` form, which `VertexRouterActorConfig` accepts. Both are fixed at the normalizer, so all of them agree.
38+
39+
- **Arango connection: drop redundant `cast(dict[str, Any], …)` after `isinstance(..., dict)`.** `ty` already narrows those values; the casts were noise. It kept its correct meaning — a tabular result row — in `db/`, `data_source/sql.py`, `query/` and `filter/select.py`, while also naming the ingestion data unit (which is a nested observation at a `LocationIndex`, not a row) and a line in a declaration table. The `merge_row_doc_with_transform_buffer``merge_observation_with_transform_buffer` rename had already picked the vocabulary; this finishes it.
40+
41+
`MergeVerticesOp.allow_row_fusion` and `ComposeManifestsOp.allow_row_fusion` are now **`allow_observation_fusion`** — what fuses is two vertex observations sharing one accumulator slot, which is what the guard's own message half-said already (*"one source **document** yielded both types, so the merged **rows** fuse"*). `AlignmentRow` is **`AlignmentAttribute`** and `IdentityAlignment.rows` is **`attributes`**: each entry is one canonical attribute that lowers to one `IdentityBranch`, so "attributes, in priority order" says what it is. Both renamed fields accept their old spelling as a validation alias, so recorded revisions and authored YAML keep loading and serialize out under the new name; `AlignmentRow` remains exported as a deprecated alias. Docstrings that said "per-row relation" now say "per-document"; "flat row" survives where the sentence is genuinely about a tabular encoding.
42+
43+
### Removed
44+
45+
- **`merge_row_doc_with_transform_buffer`.** Not backward compatibility — it was introduced as an alias alongside the canonical name and never called anywhere in graflo.
46+
847

948
## [1.12.1]
1049

0 commit comments

Comments
 (0)