Repository navigation
feat(shared-data): per-review author key for seeding analysis #1615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+382
−4
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a0347e0
feat(shared-data): per-review author key for duplicate-text analysis
JOY 712622d
fix(shared-data): track proposal SQL, all-or-nothing key load, drop b…
JOY 5a91063
chore: allow the author-keys proposal SQL in .gitignore
JOY d2d83d6
docs(proposals): author-keys migration after the second review
JOY c396262
fix(shared-data): leave database.types.ts to the migration mirror
JOY File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
5 changes: 5 additions & 0 deletions
5
Docs/Proposals/2026-11-06-dos-me-review-author-keys-rollback.sql
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| -- ROLLBACK the PROPOSAL 2026-11-06-dos-me-review-author-keys.sql (DOS/Rate#1614). | ||
| -- Safe to run twice. The analyzer then groups authors by author_name again. | ||
| BEGIN; | ||
| DROP TABLE IF EXISTS rate.review_author_keys; | ||
| COMMIT; |
26 changes: 26 additions & 0 deletions
26
Docs/Proposals/2026-11-06-dos-me-review-author-keys-verify.sql
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| -- VERIFY the PROPOSAL 2026-11-06-dos-me-review-author-keys.sql (DOS/Rate#1614). | ||
| -- Read-only. V1 to V4 return 0. | ||
|
|
||
| -- V1 the table exists with RLS on; expect 0 | ||
| SELECT count(*) FROM (SELECT 1) x | ||
| WHERE NOT EXISTS ( | ||
| SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace | ||
| WHERE n.nspname = 'rate' AND c.relname = 'review_author_keys' AND c.relrowsecurity | ||
| ); | ||
|
|
||
| -- V2 privilege matrix: anon and authenticated have nothing, rate_worker has no DELETE or | ||
| -- TRUNCATE; expect 0 | ||
| SELECT count(*) FROM (VALUES | ||
| ('anon', 'SELECT'), ('anon', 'INSERT'), ('anon', 'UPDATE'), ('anon', 'DELETE'), | ||
| ('authenticated', 'SELECT'), ('authenticated', 'INSERT'), ('authenticated', 'UPDATE'), | ||
| ('authenticated', 'DELETE'), ('rate_worker', 'DELETE'), ('rate_worker', 'TRUNCATE') | ||
| ) AS t(role_name, priv) | ||
| WHERE has_table_privilege(t.role_name, 'rate.review_author_keys', t.priv) | ||
| OR has_any_column_privilege(t.role_name, 'rate.review_author_keys', t.priv); | ||
|
|
||
| -- V3 no stored key has a bad shape; expect 0 | ||
| SELECT count(*) FROM rate.review_author_keys WHERE author_key !~ '^[A-Za-z0-9_-]{16,128}$'; | ||
|
|
||
| -- V4 no key row points at a missing review; expect 0 | ||
| SELECT count(*) FROM rate.review_author_keys k | ||
| WHERE NOT EXISTS (SELECT 1 FROM rate.reviews r WHERE r.id = k.review_id); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # Per-review author key (DOS/Rate#1614) | ||
|
|
||
| Status: **proposal, nothing applied.** The migration goes to DOS-Me; DOS/Rate never applies it. | ||
|
|
||
| Rate-Services (DOS/Rate-Services#1073) masks `rate.reviews.author_name` and redacts contact data | ||
| before the engine inserts reviews. The analyzer told authors apart by `author_name` for the | ||
| duplicate-text and seeding checks, so masked names would merge many real authors. RS therefore | ||
| sends a key per review and the analyzer groups by it. | ||
|
|
||
| ## Ingest contract (what RS sends) | ||
|
|
||
| On each review of a `ScrapedProduct` (`reviews[]`): | ||
|
|
||
| | Field | Type | Rule | | ||
| | ----------- | ------ | ------------------------------------------------------------------------------- | | ||
| | `authorKey` | string | optional; 16 to 128 chars of `[A-Za-z0-9_-]`, else dropped (the review is kept) | | ||
|
|
||
| - Recommended value: hex of `HMAC-SHA256(secret, normalised original author name)` (64 chars). | ||
| The secret stays in RS; Rate never sees it. Use one secret for all products and platforms so | ||
| the same author keeps one key. Not reversible. | ||
| - The key is compared exactly (case-sensitive). Keep the normalisation in RS stable (for example | ||
| NFC, trimmed, lower-cased) so one author gets one key. | ||
| - Absent key: the analyzer groups by `author_name` as before. Old rows keep working. | ||
| - `author_name` is stored and shown as sent (masked or not). No UI change. | ||
|
|
||
| ## Storage | ||
|
|
||
| `rate.review_author_keys (review_id, author_key, created_at)`, not a column on `rate.reviews`: | ||
| anon has a table-level SELECT on `rate.reviews`, and a stable key would let anyone link one | ||
| author's reviews across products. The new table is readable only by `service_role` and | ||
| `rate_worker`. The importer upserts a row per keyed review after inserting the reviews (also for | ||
| reviews already stored, so a later crawl backfills) and reads them for the analysis. A write or | ||
| read failure logs a warning and the analysis falls back to `author_name`. | ||
|
|
||
| ## Analyzer | ||
|
|
||
| `reviewAuthorIdentity` (apps/importer `ai-review-analyzer.ts`): `key:<author_key>` when present, | ||
| else `name:<lower-cased author_name>`, else `id:<review id>`. Duplicate-text counts and the | ||
| near-duplicate shingle pass use it. Scoring weights are unchanged. | ||
|
|
||
| Limits: reviews stored before masking carry a raw name and get a key only when crawled again; | ||
| until then they are one identity by name and a keyed copy of the same author counts as another. | ||
| Unkeyed masked names ("N***n") can merge different authors. RS should send a key on every review. | ||
| Do not rotate the HMAC secret or change the name normalisation: a crawl returns only a window of | ||
| recent reviews, so old and new keys of one author would meet in one analysis and count as two | ||
| authors. A change needs a full re-send of every review first. | ||
| The worker role can write keys only for crawled reviews (not `origin = 'community'`). | ||
| Also: `rate.review_input_hash` includes `author_name`, so masking changes that hash (RS/DB side; the crawl path does not use it). | ||
|
|
||
| ## Files | ||
|
|
||
| | File | What | | ||
| | --------------------------------------------------- | ------------------------- | | ||
| | `2026-11-06-dos-me-review-author-keys.sql` | the migration | | ||
| | `2026-11-06-dos-me-review-author-keys-verify.sql` | read-only checks V1 to V4 | | ||
| | `2026-11-06-dos-me-review-author-keys-rollback.sql` | drops the table | |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| -- ============================================================================= | ||
| -- PROPOSAL (DOS/Rate#1614): a per-review author key, so the review analyzer can tell authors | ||
| -- apart after Rate-Services masks rate.reviews.author_name (DOS/Rate-Services#1073). A DOS-Me | ||
| -- migration to review and apply; nothing here is applied by DOS/Rate. | ||
| -- Verify: 2026-11-06-dos-me-review-author-keys-verify.sql (V1 to V4 return 0). | ||
| -- Rollback: 2026-11-06-dos-me-review-author-keys-rollback.sql. | ||
| -- | ||
| -- What it adds: | ||
| -- rate.review_author_keys one row per review that has a key: the keyed HMAC of the original | ||
| -- author name, computed by Rate-Services (the key never reaches | ||
| -- Rate). It is a separate table on purpose: anon holds a table-level | ||
| -- SELECT on rate.reviews, so a new column there would be public, and | ||
| -- a stable key would let anyone link one author's reviews across | ||
| -- products. This table has no grant for anon or authenticated. | ||
| -- | ||
| -- The importer (service_role, rate_worker) writes it and reads it for the analysis. Reviews | ||
| -- without a row are analysed by author_name as before. Nothing the user sees changes. | ||
| -- | ||
| -- Idempotent. One transaction. | ||
| -- ============================================================================= | ||
|
|
||
| BEGIN; | ||
| SET LOCAL lock_timeout = '5s'; | ||
|
|
||
| CREATE TABLE IF NOT EXISTS rate.review_author_keys ( | ||
| review_id uuid PRIMARY KEY REFERENCES rate.reviews (id) ON DELETE CASCADE, | ||
| -- hex or base64url of an HMAC: 16 to 128 characters | ||
| author_key text NOT NULL CHECK (author_key ~ '^[A-Za-z0-9_-]{16,128}$'), | ||
| created_at timestamptz NOT NULL DEFAULT now() | ||
| ); | ||
|
|
||
| ALTER TABLE rate.review_author_keys ENABLE ROW LEVEL SECURITY; | ||
| REVOKE ALL ON rate.review_author_keys FROM PUBLIC, anon, authenticated, rate_worker; | ||
| GRANT ALL ON rate.review_author_keys TO service_role; | ||
| GRANT SELECT, INSERT, UPDATE ON rate.review_author_keys TO rate_worker; | ||
|
|
||
| DROP POLICY IF EXISTS review_author_keys_service ON rate.review_author_keys; | ||
| CREATE POLICY review_author_keys_service ON rate.review_author_keys | ||
| FOR ALL TO service_role USING (true) WITH CHECK (true); | ||
| -- Like reviews and review_flags: the worker reads all, writes only keys of crawled reviews. | ||
| DROP POLICY IF EXISTS review_author_keys_worker ON rate.review_author_keys; | ||
| DROP POLICY IF EXISTS review_author_keys_worker_read ON rate.review_author_keys; | ||
| DROP POLICY IF EXISTS review_author_keys_worker_insert ON rate.review_author_keys; | ||
| DROP POLICY IF EXISTS review_author_keys_worker_update ON rate.review_author_keys; | ||
| CREATE POLICY review_author_keys_worker_read ON rate.review_author_keys | ||
| FOR SELECT TO rate_worker USING (true); | ||
| CREATE POLICY review_author_keys_worker_insert ON rate.review_author_keys | ||
| FOR INSERT TO rate_worker | ||
| WITH CHECK (EXISTS (SELECT 1 FROM rate.reviews r WHERE r.id = review_author_keys.review_id AND r.origin <> 'community')); | ||
| CREATE POLICY review_author_keys_worker_update ON rate.review_author_keys | ||
| FOR UPDATE TO rate_worker | ||
| USING (EXISTS (SELECT 1 FROM rate.reviews r WHERE r.id = review_author_keys.review_id AND r.origin <> 'community')) | ||
| WITH CHECK (EXISTS (SELECT 1 FROM rate.reviews r WHERE r.id = review_author_keys.review_id AND r.origin <> 'community')); | ||
|
|
||
| COMMIT; |
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: DOS/Rate
Length of output: 19454
Script executed:
Repository: DOS/Rate
Length of output: 43831
Script executed:
Repository: DOS/Rate
Length of output: 23238
Script executed:
Repository: DOS/Rate
Length of output: 10246
Avoid treating mixed key and name identities as different authors.
When one author has both keyed and unkeyed reviews with the same qualifying text, the analyzer counts two authors and flags both reviews as
seeding_fake. This can exclude them from ratings and increase the calculated merchant risk. Handle mixed coverage before counting distinct authors.