Skip to content

feat: spotlight-org sync from hr-syncer events#170

Draft
Joey0538 wants to merge 7 commits into
mainfrom
claude/spotlight-org-sync-design-fS2qh
Draft

feat: spotlight-org sync from hr-syncer events#170
Joey0538 wants to merge 7 commits into
mainfrom
claude/spotlight-org-sync-design-fS2qh

Conversation

@Joey0538
Copy link
Copy Markdown
Collaborator

@Joey0538 Joey0538 commented May 11, 2026

Summary

  • Replace the spotlight-org MongoDB change-stream pipeline with a JetStream consumer in search-sync-worker that reads daily batched events from hr-syncer on hr.sync.{siteID}.employees.upsert and maintains the spotlightorg-{siteID} Elasticsearch index keyed by sectId.
  • Add a shared HRSyncEvent envelope (pkg/model), HR_SYNC_{siteID} stream (pkg/stream), and subject builders (pkg/subject). Consumer-side row + ES doc + ES mapping are one struct (SpotlightOrgIndex) — no public pkg/model.Employee is introduced so it doesn't conflict with the internal repo's existing one.
  • Add a single project-wide DEV_MODE toggle that collapses every search-sync-worker ES index template to 1 shard / 0 replicas in local dev; prod values per collection unchanged. Extracts a shared customAnalyzerSettings() helper used by both spotlight templates.

Design

  • Wire format: HRSyncEvent{ Timestamp, BatchID, Gzip, Payload }. When Gzip=true the payload rides as a JSON string of base64(gzip(JSON array))json.RawMessage must itself be valid JSON, so raw binary can't sit in the envelope. Decoders unmarshal into []byte (base64-decodes) then gunzip.
  • Doc-merge upsert + omitempty: partial-field events from hr-syncer (e.g., one employee row with only sectId and a renamed sectName) produce an ES _update body containing only the changed keys. Doc-merge preserves all other stored fields without a painless script.
  • Dedup by SectID: within a batch many employees share the same section. BuildAction collapses to one ES _update per unique sectId (last-wins). Empty-SectID rows are skipped silently; all-empty batches return (nil, nil) so the handler acks with no ES write.
  • Stream ownership: HR_SYNC_{siteID} schema is owned by hr-syncer. search-sync-worker is a pure consumer and skips this stream in its bootstrap loop, matching how it already skips INBOX (owned by inbox-worker).

Commits

  1. feat(model,stream,subject): HR sync wire infrastructureHRSyncEvent, stream.HRSync, subject builders.
  2. refactor(search-sync-worker): DEV_MODE toggle and shared template helpersindexTopology, customAnalyzerSettings, threaded devMode through messages/spotlight/user-room collections and their tests. The existing spotlight template now uses the shared analyzer and gains token_chars (verified accepted on ES 8.11).
  3. feat(search-sync-worker): spotlight-org collection for HR org sync — new spotlightOrgCollection, BuildAction (envelope parse → optional gzip → unmarshal → dedup → emit ES updates), main.go wiring (SPOTLIGHT_ORG_INDEX + DEV_MODE config, HR_SYNC bootstrap skip), integration test against testcontainers, and docker-compose.yml DEV_MODE env.
  4. simplify: trim comments, dedup gzip helper, pointer-keyed dedup map/simplify polish: pointer-keyed dedup map removes per-duplicate struct copies, integration test routes through the existing makeHRSyncEventGzip helper, docstrings trimmed to the non-obvious WHYs.

Test plan

  • make test SERVICE=pkg/model — passes
  • make test SERVICE=pkg/stream — passes
  • make test SERVICE=pkg/subject — passes
  • make test SERVICE=search-sync-worker — passes (17 spotlight-org tests + all preexisting collection tests)
  • make lint — clean
  • go vet -tags=integration ./search-sync-worker/... — clean (integration build compiles)
  • BuildAction coverage 92.1% (≥ 90% target per CLAUDE.md)
  • Run make test-integration SERVICE=search-sync-worker against the testcontainers harness — pending (TestSearchSyncSpotlightOrg_Integration exercises real gzip publish → ES doc upsert → second event preserves untouched fields via doc-merge)

Coordination with hr-syncer

Wire-format contract Mat needs to match on the publisher side:

  • Subject: hr.sync.{siteID}.employees.upsert
  • Envelope: model.HRSyncEvent{Timestamp(ms), BatchID(uuidv7), Gzip(bool), Payload(json.RawMessage)}
  • Payload elements: any JSON shape whose sectId + (optional) other org fields match the consumer's SpotlightOrgIndex projection — extra fields are silently ignored.
  • For gzipped publishes the payload must be base64(gzip(JSON array)) carried as a JSON string in the envelope.

Spec / plan

  • Design: docs/superpowers/specs/2026-05-11-spotlight-org-sync-design.md
  • Plan: docs/superpowers/plans/2026-05-11-spotlight-org-sync.md

https://claude.ai/code/session_01QLFefxiCHDP24LDLQzLLG2


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new spotlight organization index to synchronize organizational and departmental data from HR events.
    • Added a development mode configuration to optimize Elasticsearch index shard and replica settings for non-production environments.
  • Documentation

    • Added specification and implementation plan for HR organization data synchronization feature.
  • Tests

    • Added comprehensive unit and integration tests covering organization data sync, including gzip payload decompression and partial field update preservation.

Review Change Stack

claude added 6 commits May 11, 2026 06:37
Brainstormed design for a new spotlight-org collection in
search-sync-worker that consumes hr.sync.{siteID}.employees.upsert
events from hr-syncer (replacing the MongoDB change-stream pipeline)
and maintains the spotlightorg-{siteID} ES index keyed by sectId
via doc-merge upserts.

https://claude.ai/code/session_01QLFefxiCHDP24LDLQzLLG2
Bite-sized TDD plan for the spotlight-org sync design committed in
6ca5f07. 17 tasks covering: Employee/HRSyncEvent models, HR_SYNC
stream + subjects, shared template helpers (indexTopology +
customAnalyzerSettings), DEV_MODE threading across all four
collections, the new spotlight-org collection (metadata + BuildAction
with dedup + gzip + partial-field merge), main.go wiring, integration
test, and docker-compose.

https://claude.ai/code/session_01QLFefxiCHDP24LDLQzLLG2
Add the shared building blocks for hr-syncer's daily HR account
synchronization:

- model.HRSyncEvent: envelope on every hr.sync.* subject (timestamp,
  batchId, gzip flag, opaque payload). Gzip=true means Payload is a
  JSON string carrying base64(gzip(JSON array)); Gzip=false means
  Payload is the JSON array embedded verbatim. Consumers type the
  payload per-subject via their own local projection structs — no
  pkg/model.Employee is introduced here so the wire stays compatible
  with the internal repo's existing Employee/Org types.
- stream.HRSync(siteID): HR_SYNC_{siteID} stream definition. Schema
  owned by hr-syncer; consumers like search-sync-worker skip it in
  their bootstrap loop the same way they skip INBOX.
- subject.HRSyncEmployeesUpsert / subject.HRSyncUsersUpsert: subject
  builders. The users subject is reserved for a separate consumer.

https://claude.ai/code/session_01QLFefxiCHDP24LDLQzLLG2
…pers

A single DEV_MODE env var now collapses every ES index template to
1 shard / 0 replicas for local dev; prod values per collection are
unchanged.

- template.go: new indexTopology(prodShards, prodReplicas, devMode)
  helper centralizes the toggle, and customAnalyzerSettings()
  factors out the analysis block shared by spotlight and (in the
  next commit) spotlight-org. The custom_tokenizer now declares
  token_chars: [letter, digit, punctuation, symbol] — verified to
  be accepted by ES 8.11 against the stale comment that claimed
  otherwise.
- messageCollection: threads devMode through; messageTemplateBody
  becomes (prefix, devMode); prod stays 4 shards / 2 replicas.
- spotlightCollection: same shape; prod stays 3 / 1; analyzer block
  now routes through customAnalyzerSettings().
- userRoomCollection: same shape; prod stays 1 / 1.

All call sites in tests (handler_test, inbox_integration_test,
spotlight_test, etc.) updated to the new signatures. main.go is
left passing false placeholders here — it gets cfg.DevMode wired
in the next commit alongside the spotlight-org registration.

https://claude.ai/code/session_01QLFefxiCHDP24LDLQzLLG2
Maintains a per-section ES index (spotlightorg-{siteID}) keyed by
sectId, sourced from hr-syncer's daily batch publishes on
hr.sync.{siteID}.employees.upsert. Replaces the previous MongoDB
change-stream pipeline.

Design highlights:
- SpotlightOrgIndex is one struct serving three roles: wire-side row
  unmarshal target, ES doc body on write, and source of truth for the
  ES mapping via esPropertiesFromStruct. Nine string fields with
  omitempty and search_as_you_type / custom_analyzer ES mapping.
- BuildAction parses the HRSyncEvent envelope, optionally
  base64-decodes + gunzips a compressed payload, unmarshals
  []SpotlightOrgIndex, dedupes by SectID keeping last-wins per
  batch, and emits one ES _update per unique sectId with
  doc_as_upsert:true. Doc-merge means partial-field events from
  hr-syncer preserve untouched stored fields — no painless script
  needed. Empty SectID rows and all-empty batches return (nil, nil)
  so the handler acks with no ES write.
- ActionUpdate is used WITHOUT external versioning; handler.go's 409
  logic for ActionUpdate depends on Version=0.
- Stream HR_SYNC_{siteID} is owned by hr-syncer; this worker is a
  pure consumer and main.go's bootstrap loop skips it the same way
  it skips INBOX.
- DEV_MODE=true collapses the new index to 1 shard / 0 replicas; prod
  uses 3 / 1.

main.go: register newSpotlightOrgCollection, default
SPOTLIGHT_ORG_INDEX to spotlightorg-{siteID}, plumb cfg.DevMode into
all four collections, and add HR_SYNC to the bootstrap skip list.

Integration test exercises gzip publish, doc upsert, and doc-merge
field preservation via the existing testcontainers harness.

docker-compose.yml: DEV_MODE=${DEV_MODE:-true} for local dev.

The design and plan docs reflect a late-stage redesign: an earlier
draft introduced a pkg/model.Employee struct, but that would conflict
on merge with the internal repo's existing Employee/Org types. The
consumer-side projection now lives in this collection only.

https://claude.ai/code/session_01QLFefxiCHDP24LDLQzLLG2
Three small improvements surfaced by /simplify:

- Drop the dedup struct copy by switching the BuildAction map from
  map[string]SpotlightOrgIndex to map[string]*SpotlightOrgIndex. On
  high-cardinality batches (e.g., 500 employees in one section) this
  removes 499 × 144 bytes of redundant struct copying per JetStream
  message.
- The integration test's inline publish closure duplicated
  makeHRSyncEventGzip's encoding logic; route it through the helper
  instead. Drops the local bytes/gzip use, so the import goes too.
- Trim docstrings on the new types and helpers. Cut narrative comments
  ("Used by every BuildAction test...", "handler.go::isBulkItemSuccess
  depends on this", "Task 14 will wire ..."), kept only non-obvious WHY
  notes the next reader actually needs (Gzip+base64 wire format,
  partial-update contract, hr-syncer ownership).

https://claude.ai/code/session_01QLFefxiCHDP24LDLQzLLG2
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Warning

Rate limit exceeded

@Joey0538 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 53 minutes and 24 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f475b54-e920-4c70-9f38-c6a3bbcff990

📥 Commits

Reviewing files that changed from the base of the PR and between f9fff45 and 98c3e80.

📒 Files selected for processing (1)
  • search-sync-worker/spotlight_org.go
📝 Walkthrough

Walkthrough

This PR adds a new spotlight-org Elasticsearch index collection to search-sync-worker that consumes HR sync events from JetStream. It introduces shared template helpers (indexTopology, customAnalyzerSettings), threads a DEV_MODE toggle through existing collections (message, spotlight, user-room), and wires the new collection into the worker with comprehensive testing and Elasticsearch doc-merge upsert semantics.

Changes

Spotlight Org Sync Implementation

Layer / File(s) Summary
Design & Implementation Plans
docs/superpowers/specs/2026-05-11-spotlight-org-sync-design.md, docs/superpowers/plans/2026-05-11-spotlight-org-sync.md
Specifications and implementation plan for consuming HR sync envelopes, maintaining spotlight-org index keyed by sectId, doc-merge upsert semantics, dev-mode topology toggling, and end-to-end verification approach.
HRSync Model & Contracts
pkg/model/hrsync.go, pkg/model/model_test.go, pkg/stream/stream.go, pkg/stream/stream_test.go, pkg/subject/subject.go, pkg/subject/subject_test.go
New HRSyncEvent envelope struct (timestamp, batchId, gzip flag, raw payload). New stream.HRSync(siteID) stream configuration and HRSyncEmployeesUpsert/HRSyncUsersUpsert subject builders with unit tests.
Shared Template Infrastructure
search-sync-worker/template.go, search-sync-worker/template_test.go
Introduced indexTopology(prodShards, prodReplicas, devMode) that collapses index topology to 1 shard/0 replicas in dev mode, and customAnalyzerSettings() providing shared tokenizer/analyzer/filter configuration with token_chars. Unit tests validate both helpers.
DevMode Threading: Messages
search-sync-worker/messages.go, search-sync-worker/messages_test.go
Updated messageCollection to store devMode flag. Modified newMessageCollection(indexPrefix, devMode) and messageTemplateBody(prefix, devMode) to use indexTopology helper for shard/replica counts. Added dev-vs-prod template test.
DevMode Threading: Spotlight
search-sync-worker/spotlight.go, search-sync-worker/spotlight_test.go
Refactored existing spotlight template to use shared customAnalyzerSettings helper. Updated spotlightCollection to store devMode. Modified newSpotlightCollection(indexName, devMode) and spotlightTemplateBody(indexName, devMode) to use indexTopology and shared analyzer helpers. Added token_chars assertion and dev-vs-prod template tests.
DevMode Threading: User Room
search-sync-worker/user_room.go, search-sync-worker/user_room_test.go
Updated userRoomCollection to store devMode. Modified newUserRoomCollection(indexName, devMode) and userRoomTemplateBody(indexName, devMode) to use indexTopology helper. Added dev-vs-prod template test.
SpotlightOrg Collection
search-sync-worker/spotlight_org.go, search-sync-worker/spotlight_org_test.go
New spotlightOrgCollection consuming hr.sync.{siteID}.employees.upsert subjects. SpotlightOrgIndex document schema with nine org fields and omitempty JSON tags. BuildAction unmarshals HRSync envelopes, decompresses gzip when Gzip=true, deduplicates by SectID (last-wins), emits ES updates with doc_as_upsert semantics. Template generation uses shared helpers. Comprehensive unit tests cover dedup, gzip, partial fields, error paths, and template topology.
Worker Wiring
search-sync-worker/main.go
Added SpotlightOrgIndex and DevMode configuration fields loaded from env vars. Default index name derived from SITE_ID. Register spotlightOrgCollection alongside existing collections, passing DevMode to all constructors. Update stream bootstrap to skip HR_SYNC creation (owned by hr-syncer). Add startup logging for new config fields.
Test Harness Updates
search-sync-worker/handler_test.go, search-sync-worker/inbox_integration_test.go, search-sync-worker/integration_test.go
Updated all collection constructor calls to pass false for devMode argument in existing tests.
Integration Test
search-sync-worker/integration_test.go
New TestSearchSyncSpotlightOrg_Integration boots ES+NATS, creates HR_SYNC stream/consumer, publishes gzipped HRSync events with employee data, consumes messages, flushes to ES, and verifies index documents and doc-merge preservation across subsequent updates.
Local Dev Configuration
search-sync-worker/deploy/docker-compose.yml
Set DEV_MODE environment variable with default value true for local development.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • hmchangw/chat#109: Prior spotlight/user-room collection refactor that this PR builds upon with additional devMode threading and template helpers.
  • hmchangw/chat#64: Initial search-sync-worker implementation that introduced the collection pattern now extended by this PR.

Suggested reviewers

  • mliu33

Poem

🐰 A worker now syncs sections from the sky,
HRSync envelopes gzipped and dry.
Deduplicate, merge, let Elasticsearch play,
Doc-merge upserts save the forgotten day. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: spotlight-org sync from hr-syncer events' accurately and concisely describes the primary change: adding spotlight organization synchronization functionality from hr-syncer events.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/spotlight-org-sync-design-fS2qh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
search-sync-worker/template_test.go (1)

22-35: 💤 Low value

Consider guarding type assertions with require.NotNil checks.

The type assertions on lines 25-34 will panic if customAnalyzerSettings() returns an unexpected structure. While test panics are acceptable failures, adding require.NotNil checks before each type assertion would make failures clearer and provide better diagnostics.

🛡️ Example: safer assertions
 func TestCustomAnalyzerSettings_Shape(t *testing.T) {
 	got := customAnalyzerSettings()
 
 	analyzer := got["analyzer"].(map[string]any)
+	require.NotNil(t, analyzer, "analyzer section must be present")
 	custom := analyzer["custom_analyzer"].(map[string]any)
+	require.NotNil(t, custom, "custom_analyzer must be present")
 	assert.Equal(t, "custom", custom["type"])
 	...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@search-sync-worker/template_test.go` around lines 22 - 35,
TestCustomAnalyzerSettings_Shape uses unchecked type assertions on the structure
returned by customAnalyzerSettings(), which can panic and obscure failures;
before each assertion, add require.NotNil checks for got, analyzer :=
got["analyzer"], custom := analyzer["custom_analyzer"], tokenizer :=
got["tokenizer"], and tok := tokenizer["custom_tokenizer"] (or equivalent
intermediate values) to ensure those keys exist and are non-nil, then proceed
with the type assertions and existing assert.Equal checks in the
TestCustomAnalyzerSettings_Shape function.
search-sync-worker/spotlight_org.go (1)

135-142: 💤 Low value

Optional: surface gzip.Reader.Close() error instead of silently discarding via defer.

gr.Close() can return a checksum/truncation error after the body has already been read. With defer gr.Close() the error is dropped, so a corrupt-tail payload silently parses as a successful (possibly truncated) JSON. Low-impact in practice — io.ReadAll typically surfaces the same error first — but worth tightening since this is the trust boundary for an external publisher.

♻️ Suggested rewrite
 func gunzipBytes(b []byte) ([]byte, error) {
 	gr, err := gzip.NewReader(bytes.NewReader(b))
 	if err != nil {
 		return nil, err
 	}
-	defer gr.Close()
-	return io.ReadAll(gr)
+	data, readErr := io.ReadAll(gr)
+	closeErr := gr.Close()
+	if readErr != nil {
+		return nil, readErr
+	}
+	if closeErr != nil {
+		return nil, closeErr
+	}
+	return data, nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@search-sync-worker/spotlight_org.go` around lines 135 - 142, The current
gunzipBytes function defers gzip.NewReader.Close(), which discards any Close()
error (e.g., checksum/truncation) — change the function to explicitly call
io.ReadAll(gr) into a variable, then call gr.Close() and if Close() returns a
non-nil error return that error (or if both Read and Close produce errors prefer
the read error but surface the close error when present); update the
implementation around gzip.NewReader, io.ReadAll and gr.Close() so Close()
errors are not silently ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@search-sync-worker/spotlight_org.go`:
- Around line 135-142: The current gunzipBytes function defers
gzip.NewReader.Close(), which discards any Close() error (e.g.,
checksum/truncation) — change the function to explicitly call io.ReadAll(gr)
into a variable, then call gr.Close() and if Close() returns a non-nil error
return that error (or if both Read and Close produce errors prefer the read
error but surface the close error when present); update the implementation
around gzip.NewReader, io.ReadAll and gr.Close() so Close() errors are not
silently ignored.

In `@search-sync-worker/template_test.go`:
- Around line 22-35: TestCustomAnalyzerSettings_Shape uses unchecked type
assertions on the structure returned by customAnalyzerSettings(), which can
panic and obscure failures; before each assertion, add require.NotNil checks for
got, analyzer := got["analyzer"], custom := analyzer["custom_analyzer"],
tokenizer := got["tokenizer"], and tok := tokenizer["custom_tokenizer"] (or
equivalent intermediate values) to ensure those keys exist and are non-nil, then
proceed with the type assertions and existing assert.Equal checks in the
TestCustomAnalyzerSettings_Shape function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e7e4d29-64c0-4fdf-a305-4b2423ad6386

📥 Commits

Reviewing files that changed from the base of the PR and between 7817eb6 and f9fff45.

📒 Files selected for processing (23)
  • docs/superpowers/plans/2026-05-11-spotlight-org-sync.md
  • docs/superpowers/specs/2026-05-11-spotlight-org-sync-design.md
  • pkg/model/hrsync.go
  • pkg/model/model_test.go
  • pkg/stream/stream.go
  • pkg/stream/stream_test.go
  • pkg/subject/subject.go
  • pkg/subject/subject_test.go
  • search-sync-worker/deploy/docker-compose.yml
  • search-sync-worker/handler_test.go
  • search-sync-worker/inbox_integration_test.go
  • search-sync-worker/integration_test.go
  • search-sync-worker/main.go
  • search-sync-worker/messages.go
  • search-sync-worker/messages_test.go
  • search-sync-worker/spotlight.go
  • search-sync-worker/spotlight_org.go
  • search-sync-worker/spotlight_org_test.go
  • search-sync-worker/spotlight_test.go
  • search-sync-worker/template.go
  • search-sync-worker/template_test.go
  • search-sync-worker/user_room.go
  • search-sync-worker/user_room_test.go

gzip.Reader.Close reports trailing checksum / truncation errors that
io.ReadAll can miss on a corrupted stream. Replace the deferred close
with an explicit one so a truncated publisher payload doesn't parse as
a successful (short) JSON. Per CodeRabbit review on PR #170.

https://claude.ai/code/session_01QLFefxiCHDP24LDLQzLLG2
@Joey0538 Joey0538 marked this pull request as draft May 12, 2026 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants