feat(sync): declarative derive fields + real identity-key preview (v1.55.0) - #185
Merged
paulkr merged 2 commits intoAug 11, 2026
Merged
Conversation
….55.0) (#129) Closes the last two items on #129. 1. `derive` — flat, queryable top-level fields computed from paths already in the record: "derive": { "from_email": { "path": "messages[0].payload.headers[name=From].value", "extract": "email" } } The issue asked for the gmail `from_email` extraction to move out of a user-editable jq transform and into the built-in profile. It could not go in as a `transform`: that spawns `sh -c`, so a built-in relying on it would need jq on PATH and would be a silent no-op on Windows. `derive` is the declarative replacement — pure, cross-platform, and reusing the same path resolver `identityKeys` already uses ([] wildcards, [0] indexes, [name=From] filters), so profile authors learn one path syntax rather than two. Shipped on the built-in gmail profile. Applied inside writePageToMemory, the one point BOTH sync phases pass through, so a derived field can't flicker between the list and enriched shapes. A path resolving to nothing omits the field rather than writing null, so --where filters behave. 2. `sync test` now previews the identity keys an enriching profile ACTUALLY resolves. It previously ran against list-shape samples, where participant paths don't exist yet, and reported zero keys plus a note explaining they'd resolve later — true, but it never showed what. It now spends one detail call on the first sample and previews the merged shape, falling back to the old behaviour if that call fails. The fetch reuses enrichSingleRow and the same resultsPath/fields/exclude/ merge handling as the real phase (exported as enrichOneForPreview), because a preview that built its own request would drift from what a sync writes — worse than no preview. 24 new tests: the resolver's edge cases (fan-out, missing paths, non-scalar values, email extraction), the built-in gmail profile, and an integration test against live PGlite proving the fields reach the stored record — the wiring is where a silent no-op would hide.
…d-to-sync-profiles
paulkr
approved these changes
Aug 11, 2026
paulkr
deleted the
feature/int-4408-add-declarative-derive-field-to-sync-profiles
branch
August 11, 2026 15:41
paulkr
pushed a commit
that referenced
this pull request
Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #129 · INT-4408
Ships the last two items on #129 — the ones I'd deferred out of #181.
1.
derive— flat fields without a shell#129 asked for the gmail
from_emailextraction to move out of a user-editablegmail-extract.jqand into the built-in profile.It could not go in as a
transform.transformis the only existing way to produce a derived field, and it runs asspawn('sh', ['-c', cmd])— so a built-in relying on it would requirejqon every user's PATH, and on Windows would ENOENT and silently resolve null, leaving the sync reporting success with the field absent. Not something to ship in a default profile.So this adds a declarative alternative:
identityKeyspath resolver —[]wildcards,[0]indexes,[name=From]filters. One path syntax for profile authors, not two.extract: "email"applies the same address extractionidentityKeysuses foremail-prefixed values, so a display-name header yieldsjane@acme.com.--wherefilters and leave every record carrying dead keys.Applied inside
writePageToMemory— deliberately, because that's the one point both sync phases pass through. Phase 1 writes list-shape records and phase 2 writes enriched ones; a derived field applied in only one would flicker between runs.Shipped on
profiles/gmail/gmailThreads.json, which is #129's actual ask.2.
sync testpreviews what actually resolvesPreviously, for an enriching profile, the preview ran against list-shape samples where the participant paths don't exist yet — so it reported zero keys plus a
resolvesAfterEnrichnote. Accurate, but it never showed the author what would resolve, which is what acceptance criterion 4 wanted.It now spends one detail call on the first sample and previews the merged shape, flagged
previewedAfterEnrich. If that call fails (rate limit, permissions, an unreachable detail action) it falls back to the previous behaviour, so the command never gets worse.The fetch reuses
enrichSingleRowand the sameresultsPath/fields/exclude/mergehandling as the real phase, exported asenrichOneForPreview. A preview that constructed its own request would drift from what a sync actually writes, which is worse than no preview.Worth a reviewer's opinion: this makes
sync testperform a real (read-only) API call for enriching profiles, where before it was purely a dry check. That's the reading closest to the acceptance criterion, but if you'd rather it were behind--deepit's a small change.Tests
24 new, 457 total, 0 skipped, all passing.
derive.test.ts— resolver edge cases: fan-out, missing paths, non-scalar values, email extraction with and without a display name, the built-in gmail profile, and an assertion that no built-in requires a shell.mem-writer.test.ts— an integration test against live PGlite proving derived fields reach the stored record and that a missing path writes nothing. The wiring is where a silent no-op would hide, so it's covered separately from the resolver.Notes
guide-content.ts(new "Derived fields" section placed before Record Transform, since it's what you should reach for first) andskills/one/SKILL.md. No CLI help change —deriveis a profile field, not a flag.