Skip to content

feat(dotc1z): surface discovered_at through reader#1025

Draft
manojacs wants to merge 1 commit into
mainfrom
athreya/surface-grant-discovered-at
Draft

feat(dotc1z): surface discovered_at through reader#1025
manojacs wants to merge 1 commit into
mainfrom
athreya/surface-grant-discovered-at

Conversation

@manojacs

Copy link
Copy Markdown
Contributor

What & why

c1z entries store a per-entry discovered_at — the time the connector actually observed a grant. It's persisted in the storage layer but stripped when storage records convert to c1.connector.v2 protos: pebble.V3GrantToV2 never copies r.GetDiscoveredAt(), and v2.Grant has no field to carry it. So c1 can't read it.

c1 needs it: its uplift deletes optimistic "overlay" grant records by comparing overlay.created_at against a single coarse per-sync timestamp, which can delete an overlay before the connector has actually re-observed the grant (→ a user loses access). The real fix compares against the per-entry discovered_at. This PR stops dropping it — it surfaces it through the reader.

Mechanism chosen: A — additive optional reader capability

New interface connectorstore.GrantDiscoveredAtReader, in the exact mold of the existing EntitlementGrantDigestReader / ExpansionGrantLister:

type GrantDiscoveredAtReader interface {
    GetGrantDiscoveredAt(ctx context.Context, grantID string) (discoveredAt *timestamppb.Timestamp, found bool, err error)
}
  • Type-asserted, Pebble-implemented. Only the Pebble (v3) engine implements it; callers type-assert and fall back on failure.
  • Addressed by the same grantID GetGrant accepts (stored external id, or the legacy reconstructed concat). The Pebble impl reuses GetGrantRecord and returns rec.GetDiscoveredAt().
  • v2.Grant is untouched → the c1.connector.v2 API contract is unchanged, keeping the review surface narrow.

Why A over B (adding a field to v2.Grant): B is simpler for c1 to consume but mutates the connector API contract (proto field on a wire type shared by every connector) — a much wider blast radius for a value only c1's uplift needs. A is fully additive and mirrors how every other engine-specific read capability in this package is already exposed.

Semantics

  • Value is the STORED discovered_at, stamped once at first write and carried forward across re-syncs / expander rewrites — never a fresh time.Now().
  • found=false (nil error) = "no such grant"; an ambiguous concat id stays an error (never a guess), matching GetGrant.
  • found=true with a nil timestamp = a grant stored without a stamp (legacy) → caller treats as "unknown", not epoch zero.

Scope

  • Grants only (the must-have). Entitlements also carry discovered_at in storage — a parallel GetEntitlementDiscoveredAt is a cheap follow-up if the "absence/coverage" signal ever needs it, deliberately left out here to keep the surface minimal.
  • SQLite (deprecated) is intentionally not implemented — a failed type assertion is the caller's "unavailable, fall back" signal, exactly like EntitlementGrantDigestReader.

Files touched

  • pkg/connectorstore/connectorstore.go — new GrantDiscoveredAtReader interface + doc.
  • pkg/dotc1z/engine/pebble/adapter_reader.go — Pebble GetGrantDiscoveredAt impl.
  • pkg/dotc1z/engine/pebble/adapter.go — compile-time interface assertion.
  • pkg/dotc1z/engine/pebble/grant_discovered_at_reader_test.go — exact-seed round-trip, unknown-id, nil-stamp.
  • pkg/dotc1z/grant_discovered_at_reader_test.go — cross-engine: Pebble returns stored value (stable across reads); SQLite cleanly reports unavailable.

Tests

go test ./pkg/connectorstore/... ./pkg/dotc1z/ ./pkg/dotc1z/engine/pebble/   # ok

All new tests pass; full pkg/dotc1z + Pebble suites green. Changed files are lint-clean.

🤖 Generated with Claude Code

c1z entries store a per-entry discovered_at (the time the connector
actually observed the datum), but it is stripped when storage records
convert to c1.connector.v2 protos: V3GrantToV2 never copies
r.GetDiscoveredAt(), and v2.Grant has no field to carry it. c1 needs it
to compare optimistic overlay grants against the real observation time
instead of a coarse per-sync timestamp.

Add GrantDiscoveredAtReader, an additive optional Reader capability in
the exact mold of EntitlementGrantDigestReader / ExpansionGrantLister:
type-asserted, Pebble-implemented, addressed by the same grant id
GetGrant accepts. It returns the STORED discovered_at verbatim (never a
fresh now()) and leaves v2.Grant untouched, so the connector API
contract is unchanged. The deprecated SQLite engine does not implement
it; a failed type assertion is the caller's "unavailable, fall back"
signal.

Round-trip tests cover both engines: Pebble returns the stored value
(exact seeded timestamp, stable across reads), SQLite cleanly reports
unavailable.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
@manojacs manojacs closed this Jul 21, 2026
@manojacs
manojacs deleted the athreya/surface-grant-discovered-at branch July 21, 2026 21:39
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

General PR Review: feat(dotc1z): surface discovered_at through reader

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 3a4e5775f92f.
Review mode: full
View review run

Review Summary

Scanned the full PR diff for security and correctness. This is a fully additive change: a new standalone GrantDiscoveredAtReader interface, a Pebble-only implementation, a compile-time assertion, and tests — no existing exported API, proto, serialized-state, default, or dependency surface is touched. The implementation delegates to GetGrantRecord and correctly maps pebble.ErrNotFound to found=false while letting ErrAmbiguousExternalID propagate as an error, matching the documented GetGrant resolution contract. No new issues found.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@manojacs
manojacs restored the athreya/surface-grant-discovered-at branch July 21, 2026 22:50
@manojacs manojacs reopened this Jul 21, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

// GetGrantDiscoveredAt implements connectorstore.GrantDiscoveredAtReader.
// It returns the discovered_at stored on the grant addressed by grantID
// — the value stamped when the grant was first written and preserved
// across re-syncs and expander rewrites, NEVER a fresh now(). It reads

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is incorrect. Discovered_at is basically the time at which we crawled the row. If we start a new sync, the same resource will have a new discovered_at.

return nil, false, nil
}
return rec.GetDiscoveredAt(), true, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is uplift going to get the grant, then call GetGrantDiscoveredAt for that grant? If so, that would double the number of grant reads for a given uplift.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we start a new sync, the same resource will have a new discovered_at.

Right. Since the problem right now is flipping overlays, part of me is thinking if we should do this only for overlays.

@kans

kans commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

This would be fixed if we exposed a v3 set of APIs? We should just do that IMO with all of them. It would be cleaner and sound.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants