Skip to content

chore(release): promote dev to main - #1181

Merged
JOY (JOY) merged 15 commits into
mainfrom
dev
Oct 28, 2026
Merged

JOY (JOY) merged 15 commits into
mainfrom
dev

Conversation

@JOY

@JOY JOY (JOY) commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

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

  • 🚀 New feature
  • 🐛 Bugfix
  • 🧹 Refactor
  • ⚡ Performance
  • 📚 Documentation
  • ⚙️ CI / Configuration

🔍 Changes Walkthrough

File Summary of Changes
.github/workflows/ci.yml Adds scripts/ops/incident-report.test.mjs to the CI test suite to ensure the new incident reporting script is validated.
.github/workflows/incident-report.yml Replaces a complex inline Bash script with a call to scripts/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.md Updates agent instructions to clarify that incident issues only auto-close when all previously failing jobs pass again; explicitly notes that skipped jobs do not count as recovery.
Docs/Proposals/2026-10-27-item-variant-listing-model.html New File. A styled HTML summary (in Vietnamese) of the proposed Item/Variant/Listing data model, intended for stakeholder review.
Docs/Proposals/2026-10-27-item-variant-listing-model.md New File. A detailed technical design document (in English) proposing a new items table above the existing master_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
Loading

JOY (JOY) and others added 15 commits October 27, 2026 23:35
…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>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

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.

@vercel

vercel Bot commented Oct 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
rate Ready Ready Preview Oct 28, 2026 7:06am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Oct 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 526e60f1-a0f7-45c3-8198-f2621e91c633

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@github-actions github-actions Bot added ci CI/CD workflows and pipelines docs Documentation changes area:frontend Frontend / Next.js / UI area:shared Shared packages / contracts / configs area:crawlers area:database labels Oct 28, 2026
dos[bot]
dos Bot approved these changes Oct 28, 2026

@dos dos Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⏱️ 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.

This was referenced Oct 28, 2026

This branch was successfully deployed

1 active deployment
Preview — 7d106f24 Deployed Oct 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:crawlers area:database area:frontend Frontend / Next.js / UI area:shared Shared packages / contracts / configs ci CI/CD workflows and pipelines docs Documentation changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant