Repository navigation
Conversation
…1130) (#1133) * ci(ops): merge-when-green writes a commitlint-clean squash message (#1130) GitHub's default squash message keeps each commit's long lines and mixes the co-author trailers into the body, so the head of dev could fail the commitlint check of the next promotion PR (#1128). The script now sends commit_title (the PR title with its number, cut at a word if it would pass 100 characters) and commit_message (each commit as a list item, wrapped at 100 characters, trailers gathered once into a footer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(ops): refuse unfit titles, send the merge as JSON on stdin (#1130) Review follow-up on PR #1133. A title that is not a conventional header or does not fit "<title> (#n)" in 100 characters is refused before waiting, instead of being cut into an empty or start-case subject. The merge fields go to gh on stdin, so a long squash message is not over the Windows command line limit. Long words split between characters, a trailer over 100 characters is left out instead of wrapped, a PR whose commits cannot all be listed is refused, and the commit-list retry counter resets after a success. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(release): merge-when-green refuses titles that cannot carry (#n) (#1130) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…1135) * feat(web): full-width category layout with a sticky filter sidebar (#1132) Category listing pages now use a 1800px frame instead of the 1320px container: a 280-300px filter sidebar that sticks under the navbar and scrolls on its own, and a results grid that adds columns as the screen widens (3 at 1280-1440px, 4 at 1920px). The deal radar moves below the results so the product list starts right under the toolbar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): fit the catalog sidebar and cards at 1280px in every locale (#1132) Keep the sidebar at 280px, lower the card minimum to 290px so 1280px still gets three columns, let the radar badge and the card buttons wrap on narrow cards (long cs labels), and stop the keyword header from breaking over three lines. Adds the before/after screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): let the page scroll past the catalog sidebar's end (#1132) Review P2: overscroll-behavior: contain stopped the wheel from moving the page while the pointer was over the filter sidebar once it reached its end. Drop it and document the sticky offset and the badge wrap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* docs(design): Item / variant / listing data model proposal (#1129) Survey of the per-storage master model across DB, importer, web/API/extension and Rate-Services, a recommended schema (Item above master_products, which stays the variant level; NULL cluster_id = unknown variant), impact, a phased DOS-Me migration plan with rollback and verification queries, open questions and a task breakdown. Proposal only: no runtime code, no migrations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(design): address review findings on #1129 design Single-variant Items keep their unknown/standard master (earbuds no longer collapsed), variant_key keeps the literal key format, pg_trgm schema looked up at run time, archived masters backfilled, safe M2/M3 rollbacks, idempotent constraints, compatibility sync triggers, Item-level vote/rating path, trust view and URL-lookup consumers, Rate-Services ordering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(design): round-2 review fixes on #1129 SQL drafts M3 multi-size rule ignores archived sizes and earbuds; sync trigger re-derives item and variant on re-key and publishes the Item; M1 and M2 ship as one migration with an Item status recompute; SECURITY DEFINER sync triggers so rate_worker needs no write grant on rate.items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(design): retrigger checks after pr title fix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#1134) (#1157) * fix(ci): close an incident only after every failed job succeeds again (#1134) incident-report.yml closed the incident on any passing push run, even when the jobs that failed were only skipped on it. The logic moves to scripts/ops/incident-report.mjs: the incident issue keeps a hidden marker of the jobs still waiting to recover, a job leaves it only by concluding success, and the issue closes when the marker is empty. Otherwise it comments which jobs are still unverified. An incident without a marker closes only on a run where every job succeeded. Unit tests run in CI. * fix(ci): harden incident recovery tracking from review (#1134) - Read the last marker and strip every marker, so one quoted from a commit title cannot replace the real state. - A failure with no failing job names keeps the jobs already pending. - A matrix name recovers only when every job of that name succeeded. - Read the jobs of the event's run attempt, not of a later re-run. - A pending job no longer listed in the workflow (renamed or removed) does not keep the incident open forever. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and's (#1137) (#1161) * fix(normalizer): a Watch band's size list across "," or "|" stays the band's (#1137) A band phrase that ends in a size now runs on over the lone sizes listed after it with "," or "|", so "Sport Loop 42mm | 46mm" names two band sizes and keeps the size-less key, and "(Sport Band 42mm, 46mm) Case 42mm" keys -42mm. * fix(normalizer): a band size list runs on over sizes with a fit or trailing text (#1137) Review follow-up: a band phrase holding any size starts the list, and a part that starts with a size continues it, so "42mm S/M, 46mm M/L", "42mm, 46mm Chính Hãng" and "[Sport Band 42mm, 46mm] Mới" stay size-less. * fix(normalizer): a listed size that names the case ends the band list (#1137) Codex review P1: "Sport Band 46mm, 42mm Aluminum Case" lost its case size after 61aadd1. A part with "case", "mặt" or "vỏ" now ends the list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1144 #1145) (#1165) * fix(web): show only true review, risk and median numbers (#1139 #1140 #1145) - Leaderboard: the "blocked fake/spam reviews" count is the stored reviews the analyser excluded for that product, not total_reviews (the marketplaces' rating count) minus authentic reviews. Same for the leaderboard and versus API field filtered_noise_review_count (#1139). - Merchant page: the DOSafe badge and status use the home cards' cut-off (below 20 clean, 20-49 caution, 50+ alert) through one shared helper, and 0 analysed reviews no longer reads as "100% authentic" (#1140). - Deal radar: the median sentence names the number of listings behind it instead of a fixed "10 platforms", and the home radar counts listings and merchants separately (#1145). * fix(web): composite score starts from the buyer score the header shows (#1144) The reviews section blended the raw weighted mean (5.00) while the header showed the Bayesian buyer score (4.54), so a product with only buyer reviews showed 5.0 next to 4.5. The composite now reads the Bayesian score; the 60/40 weights and the 0.6 confidence threshold are unchanged. The buyer-score maths moves to a pure helper so a unit test can pin it. * fix(web): no multi-platform floor badge from fewer than 3 offers (#1145) With 2 offers the median is their midpoint, so the cheaper one was always "Đáy giá đa sàn". genuine_all_time_low now needs 3+ offers; a cheaper than median offer among fewer is a plain best_market_deal, and every radar badge (product, leaderboard, compare, watchlist, exports) names it "below median" instead of the multi-platform floor. * fix(web): merchant directory deal badge says below median, not cross-store floor (#1145) The badge counts every genuine deal of the merchant, including below-median offers among only 2 listings, so it cannot call them all price floors. * revert(web): keep genuine_all_time_low unchanged in this PR (#1145) This reverts commit cce676f. The PO keeps the deal verdict rule as is until JOY decides on the 3-offer threshold; this PR only fixes the copy for #1145. * fix(web): review round 1 for the display truth fixes (#1139 #1145) - Leaderboard and versus APIs show no blocked count instead of failing the whole response when the filtered-review read fails. - The merchant profile deal count no longer calls every below-median deal a cross-store floor (same wording as the directory badge). - No median sentence when the offer count is unknown, rather than "2". - Offers with neither a merchant id nor a name count as their own seller instead of merging per platform. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1167) * feat(shared-data): Item and Variant schemas, model_key helpers (#1149) Add the Item / variant key rules from the design doc (#1129, section 4): composeModelKey and splitModelKey (model_key = item_key:variant_key), isItemKey / isVariantKey / isModelKey / isSizedVariantKey, and the ItemSchema, VariantSchema and VariantSourceSchema zod contracts. The helpers are zod-free and exported from both the root entry and @repo/shared-data/rules. Tests round-trip every key in the importer fixtures. Nothing consumes them yet. * test(shared-data): stricter fixture key scan, note legacy variant_key rule (#1149) * feat(shared-data): allow _ in model_key segments, as slugify emits (#1149) PO decision: the key shape accepts '_' in all three segments, because the normalizer's slugify keeps it and real keys carry it. Empty segments (e.g. samsung::unknown) stay rejected. The design doc's 7.3 inventory regex now matches the helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) * feat(importer): read-only Item migration inventory report (#1148) Runs the M0 checks of the Item / variant / listing design (section 7.3) over the anon-readable tables, and runs today's ProductNormalizer on each seed, master name and listing title to find key drift, which SQL cannot. SELECTs only, with the anon key. * fix(importer): inventory report review fixes (#1148) NULL brand or category is not an Item conflict, as in SQL count(DISTINCT). TB and suffixed variant keys count as sized. Slug checks now also flag an Item whose masters strip to different slugs and a slug still sized after stripping. Listings with no stated size get their own drift bucket. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ellers (#1141) (#1164) * fix(web): product page reads the stored market stats every page shows (#1141) The product page, its meta description and JSON-LD computed the "from" price, range and seller count from its own offer list, which also holds sold-out and discontinued listings at their last price. Catalogue cards, compare, versus, catalogue search and sort and the API read the stored master stats, which rate.refresh_master_market_stats computes from crawled in-stock listings without bait prices. Buds3 Pro showed 2,694,000 from 3 sellers on its page and 3,140,000 from 2 everywhere else: both FPT Shop listings are not counted by the stats. - productMarketStats() is the one reader of min_price / max_price / merchant_count for the product page, metadata and JSON-LD. - Offers carry rate.products.availability; the offer list labels sold-out listings, lists them last and leaves them out of the bait check and the price spread. The price history's current low ignores them too. - No AggregateOffer with a lowPrice of 0; a product with no in-stock listing says so instead of "0 ₫". * fix(web): review follow-ups for the single market-stats source (#1141) - Metadata, OG and JSON-LD description: no "from 0 ₫" when no store has the product in stock (new metaDescNoStock / ogDescNoStock). - The product page and API radar note use in-stock offers only. - Watchlist prices and target alerts read the stored min_price. - Price history judges bait prices among in-stock listings for both the chart and its current low; the page shows its own "from" price there. - A stored price always has at least one seller. * fix(web): E2E seed stores the bait-free min_price; quiet availability fallback (#1141) - The E2E bait-price phone stored the bait 1,250,000 as min_price, the pre-#802 state. rate.refresh_master_market_stats writes 3,030,000 since DOS-Me 20261024060000, and the page now reads the stored stats, so the seed stores what production writes. - The listing availability read warns once and falls back to "unknown" when the column is missing (the E2E schema until #1147) instead of logging a paged-read error per page. * chore(web): restate the previous subject in lower case for commitlint (#1141) The previous commit seeds the bait-free min_price for E2E and quiets the availability fallback; its subject started with "E2E" (subject-case). * Merge remote-tracking branch 'origin/dev' into claude/core-1141-market-stats-source # Conflicts: # apps/web/package.json Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1169) * chore(db): mirror the DOS-Me rate migrations into the reference copy (#1147) Copies seven rate-schema migrations from DOS/DOS.Me verbatim and adds the authenticator role to the replay fixture, which 20261018183000_rate_worker_role grants rate_worker to. * test(db): give the replay fixture's auth.users the columns Supabase has (#1147) 20261023090000_rate_review_vote_eligibility reads is_anonymous, phone_confirmed_at and created_at. * test(e2e): log PostgREST in as the fixture's authenticator role (#1147) * chore(shared-data): regenerate database.types.ts from the mirrored migrations (#1147) Generated by pnpm gen:db-types on CI (postgres-meta v0.99.0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1142) (#1163) * fix(web,importer): per-category spec templates for earbuds and watches (#1142) Earbuds rendered with the phone spec template, filled by the AI enricher with a 1 inch screen, 1GB RAM, 64GB ROM and microphones as cameras; a cellular Apple Watch showed 5G. - @repo/shared-data/rules: getCategorySpecTemplate / applyCategorySpecTemplate list the groups, fields, key_specs and flat columns each category has. Earbuds: audio, battery, connectivity, design, availability, regulatory. Smartwatches: no camera, no 5G. Laptops: no 5G. Phones and tablets keep all ten groups. - Product page: groups render in the category's order with renumbered headings and a matching group count; the memory and camera tiles and rows a category lacks are gone. The page, its peers, catalogue cards and home cards all read the product through the template. - Importer: the enricher result and every master insert/update go through the template, so new and re-enriched masters stop storing phone fields for earbuds and 5G for watches. The prompt says so too. - Tests: shared-data per-category templates, web headings and wiring, EntityMatcher writes, and a hermetic E2E with a phone-shaped seeded bud. * Merge remote-tracking branch 'origin/dev' into claude/cranky-williams-fe031c # Conflicts: # apps/web/src/app/[locale]/smartphones/page.tsx * fix(web,importer): review round 1 for category spec templates (#1142) - Watches: hiding 5G is display-only. The importer keeps a stored 5G flag and 5G bands (a few watches carry RedCap 5G) and drops only the camera. The bands row stays on watches as "4G bands". - Earbuds keep the hardware group with the chipset row only, so the audio chip (H2, BES2700) survives writes and the chip tile has a fallback. - Laptops stay on the full template (some ship with 5G modems). - Re-enrichment scopes the write by the stored category only, never by a guess from the title. - Watches keep key_specs.ram; the generic's narrowing is documented. * Merge remote-tracking branch 'origin/dev' into claude/cranky-williams-fe031c # Conflicts: # apps/web/package.json # apps/web/src/app/[locale]/page.tsx * Merge remote-tracking branch 'origin/dev' into claude/cranky-williams-fe031c # Conflicts: # apps/web/package.json # apps/web/src/app/[locale]/smartphones/[slug]/page.tsx # packages/shared-data/src/__tests__/shared-data.test.ts # packages/shared-data/src/rules/index.ts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…old-out listings (#1176) * fix(web): batch offers read availability; radar and best offer skip sold-out listings - getMasterProductOffersBatch reads rate.products.availability per chunk and attaches it, like the single-product read (#1170). - The deal radar drops out_of_stock and discontinued offers before the market floor, spread, deals and traps, for every caller (#1170). - selectBestOfferFromListings decides on availability, not stock: crawlers report stock 0 when they read no count (#1171). - The availability read no longer falls back to unknown on a failed read or a missing column; it throws RateDataUnavailableError like the offer reads (#1174). * test(web): restore fetch after the availability block Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(proposals): extension as a primary data source (#1175) Decision document for JOY (Vietnamese, dark HTML) with an English companion. Covers the current extension and ingest quarantine, how Honey, Keepa and BeeCost source data, a minimal observation payload, consent and store review, anti-poisoning, cold start, per-marketplace coverage, legal notes, why the client is public while verification stays in DOS/Rate-Services, options, milestones, metrics and open questions. No product code changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…out offers (#1180) * fix(web): extension verdict and cheaper-elsewhere skip sold-out offers - evaluateExtensionPriceVerdict drops out_of_stock and discontinued offers before the market reference, the cheaper-offer count and the implausibly-low check, for the extension lookup and the inspect API. - lookupExtensionProduct's cheaper-elsewhere list and its fallback match use in-stock offers only; a URL-matched listing is still judged. - inspectProductOrShopUrl's fallback match skips sold-out listings too. * fix(web): Deep Inspect fallback match skips sold-out offers; test a sold-out own page Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ODEL (#1179) * feat(importer): item-first matcher with match_item behind RATE_ITEM_MODEL (#1151) - Normalizer emits item_key, variant_key, item_name, item_slug, variant_known, variant_source and keyable. A title with no model keys on the `unkeyed` placeholder instead of an empty segment (`samsung::unknown`) and is rejected before any write (UnkeyableProductTitleError). - EntityMatcher.resolveItemAndVariant: Item by itemIdHint, exact item_key, then match_item on Item names; exact (item_id, variant_key); a storage-less title on a multi-size Item gets no variant (#1105). - Pipeline writes item_id and variant_source, nullable cluster_id, itemIdHint, archived masterIdHint mapped to its Item, keeps a manual variant, refreshes Item stats. - Everything new sits behind RATE_ITEM_MODEL=off|item|nullable_variant, default off; mode off keeps today's matching and writes. * fix(importer): keep manual variants in mode item, never reseed archived unknown (#1151) Review fixes on the item-first matcher: - Keep a hand-set variant in both flag modes unless the title states a size. - Refresh the stats of the variant a listing leaves. - An archived unknown variant is never reseeded or adopted: no variant in nullable_variant, an error in item. - A fuzzy Item hit only links to existing variants; a missing variant is seeded under the listing's own key, as Tier 2 only links to existing masters. - A sizeless line keyed unknown reuses a seeded standard variant. * fix(importer): sticky manual and evidence variants, archived variants in nullable mode (#1151) Codex adversarial review fixes: - A hand-set variant always stays; a title/sku/single_size variant stays when a later crawl has no size evidence; legacy links do not. - nullable_variant never links a listing to an archived variant. - An unkeyable title is only rejected without masterIdHint, so refreshes of hinted listings keep working in mode off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
There was a problem hiding this comment.
⏱️ Code Review completed (84 eligible files · 641,253 chars · 2 model call(s) · 22 PR unit(s))
Whole diff with bounded context at base 69e8b84 / head 7d106f2.
Excluded by noise policy: Docs/Screenshots/1132-category-layout/after-1280.jpg, Docs/Screenshots/1132-category-layout/after-1440-scrolled.jpg, Docs/Screenshots/1132-category-layout/after-1440.jpg, Docs/Screenshots/1132-category-layout/after-1920-top.jpg, Docs/Screenshots/1132-category-layout/after-1920.jpg, Docs/Screenshots/1132-category-layout/after-390.jpg, Docs/Screenshots/1132-category-layout/before-1280.jpg, Docs/Screenshots/1132-category-layout/before-1440-scrolled.jpg, Docs/Screenshots/1132-category-layout/before-1440.jpg, Docs/Screenshots/1132-category-layout/before-1920-top.jpg, Docs/Screenshots/1132-category-layout/before-1920.jpg, Docs/Screenshots/1132-category-layout/before-390.jpg.
✅ CLEAN_PASS: Complete diff reviewed
Context investigation was bounded to the explicitly requested snapshot paths.
Promote dev to main.
Merge with a merge commit, never squash (Docs/Resources/RELEASE.md).
📌 TL;DR
This PR refactors the CI incident-reporting workflow into a dedicated Node.js script to improve reliability and security, and introduces a comprehensive design proposal (in both Markdown and HTML) for a new Item/Variant/Listing data model to resolve product duplication and pricing issues.
🎯 Type of Change
🔍 Changes Walkthrough
.github/workflows/ci.ymlscripts/ops/incident-report.test.mjsto the CI test suite to ensure the new incident reporting script is validated..github/workflows/incident-report.ymlscripts/ops/incident-report.mjs. Adds a secure checkout step (sparse checkout, no credentials) to prevent untrusted code execution. Updates comments to clarify that incidents only close when all previously failing jobs pass (skipped jobs do not count as recovery).AGENTS.mdincidentissues only auto-close when all previously failing jobs pass again; explicitly notes thatskippedjobs do not count as recovery.Docs/Proposals/2026-10-27-item-variant-listing-model.htmlDocs/Proposals/2026-10-27-item-variant-listing-model.mditemstable above the existingmaster_products(variants) table. It outlines schema changes, migration strategy (M0-M6), and impacts on the web app, importer, and API to fix product duplication and pricing logic.📊 Architectural Flow
sequenceDiagram participant CI as GitHub Actions (CI) participant Workflow as incident-report.yml participant Script as incident-report.mjs participant GH as GitHub API Note over CI, Workflow: Triggered by workflow_run (failure/success) CI->>Workflow: Passes RUN_ID, SHA, CONCLUSION, etc. Workflow->>Workflow: Checkout scripts/ops (secure, no creds) Workflow->>Script: Execute node scripts/ops/incident-report.mjs Script->>GH: Check if SHA is current head of branch alt SHA is stale Script-->>Workflow: Exit (skip) else SHA is current Script->>GH: Fetch job statuses for RUN_ID alt CONCLUSION is success Script->>GH: Check if all previously failing jobs now passed alt All failing jobs passed Script->>GH: Comment & Close incident issue else Some jobs still failing/skipped Script-->>Workflow: Do nothing (wait for full pass) end else CONCLUSION is failure Script->>GH: Find or create incident issue Script->>GH: Comment with failing job details end end