# Changelog of OuedKniss Multi Scraper (`hamza325/ouedkniss-multi-scraper`) Actor

- **URL**: https://apify.com/hamza325/ouedkniss-multi-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/hamza325/ouedkniss-multi-scraper.md

## Changelog

All notable changes to the OuedKniss Multi-Scraper Actor are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### \[3.1] - 2026-09-17

#### Added

- **Category nodes now carry their browse `url`.** Rows from `Category menu` (`categories`) include a ready-to-use category page link (e.g. `https://www.ouedkniss.com/automobiles_vehicules/1`), so a node can be fed straight into the **Category page URL** (`categoryUrl`) input without rebuilding the link from its slug. Rare root-level nodes that the site returns without a slug keep `url: null`. The **Category menu** dataset view shows the new column.

### \[3.0] - 2026-09-04

#### Removed

- **The seller-info row in `Seller listings` mode.** Runs previously emitted one extra row describing the seller itself (identified by `attributes.__kind === "seller"`, carrying the store's public contact block when `includeContactInfo: true`). `Seller listings` now returns listing rows only — every row is one of the seller's listings. This also removes the dataset's **Seller profile** view and the `__kind` system key.

#### Changed

- **`Include real phone + email` is effective in `Specific listing URLs` mode only.** `Seller listings` used to attach a store's public contact block to the seller-info row; without that row there is no surface for it, so the toggle now does nothing in seller mode (and, as everywhere else, prints the no-op warning at startup).

#### Migration

- Saved datasets keep their existing rows. New `Seller listings` runs produce listing rows only — delete the previous `attributes.__kind = "seller"` row if you already captured one. Re-push the Actor to pick up the new output schema.

### \[2.0] - 2026-09-04

A ground-up redesign of the input form around one rule: **every reference field takes a full page URL, pasted straight from the browser's address bar.** No bare IDs, no bare slugs, no mode-specific single-URL fields. The Console form now groups secondary inputs into collapsible sections (Filters / Limits & performance / Output & debugging) instead of one long list.

#### Removed

- **`Same seller's listings` mode (`startMode: "sameAuthor"`)** and its `Listing ID or URL` input. To scrape everything a seller has published, use `Seller listings` mode and paste the seller's page URL — reachable in one click from any listing row via `sellerProfileUrl`.
- **The separate single-listing `Listing ID or URL` input** (`similarListingId`). `Similar products` mode now reads the shared `Listing URLs` input instead.

#### Added

- **`Listing URLs` is now the single input for every mode that starts from a listing page.** `Specific listing URLs` scrapes each pasted URL with full detail data; `Similar products` fetches the products similar to each pasted URL. Both accept one or more URLs, and both fail with an error naming every invalid entry instead of silently skipping them.
- **`Similar products` provenance.** With several source listings in one run, each similar row records its source in `attributes.similarToId`, so results stay easy to group.
- **Collapsible input sections.** `Filters`, `Limits & performance`, and `Output & debugging` group the secondary inputs so the form opens with just the mode picker and its related fields.

#### Changed

- **Renamed inputs.** `Category` (`categorySlug`) → `Category page URL` (`categoryUrl`); `Seller` (`sellerId`) → `Seller page URL` (`sellerUrl`). Both now accept full `https://www.ouedkniss.com/…` URLs only — category URLs anywhere on the site work (a leading `/en/` or `/ar/` locale prefix is fine), and seller URLs must be `/store/{id}/…` or `/membre/{id}` pages.
- **URL-only enforcement.** Bare values are rejected with a validation error instead of being auto-detected: no bare numeric seller IDs (store and member ID spaces overlap, so only the URL form is unambiguous), no bare category slugs, no bare listing IDs in `Listing URLs`. Paste the URL from the address bar; that always works.
- **Empty store grids no longer fall back to a member-profile lookup.** A `/store/…` URL that matches zero listings now returns an empty dataset. The previous fallback probed the numeric ID as a member profile, which could silently scrape an unrelated seller when the store URL was dead.
- **Seller-info row timing for store runs.** The store's seller-info row is emitted when the store grid returns at least one listing (unchanged in practice — a store with listings always produces it).

#### Migration

- Saved tasks that pass `sameAuthorListingId`, `similarListingId`, `sellerId`, or `categorySlug` will see a schema-validation warning. Re-pick the mode and paste full URLs: seller page URLs into `Seller page URL`, listing page URLs into `Listing URLs`, category page URLs into `Category page URL`. Old bare-ID values cannot be converted automatically — open the corresponding page on ouedkniss.com and copy the URL from the address bar.

### \[1.7] - 2026-09-02

#### Added

- **`Similar products` mode (`startMode: "similar"`).** Fetches products algorithmically similar to a given listing via OuedKniss's `Similar` GraphQL query. Paste a numeric listing ID (e.g. `57354225`) or a full detail URL into the new `Listing ID or URL` input — the Actor extracts the ID automatically. Results are flat card projections tagged with `attributes.slot = "similar"` so consumers can distinguish them from other modes. The query returns up to 30 results in a single response (no pagination).
- **`Same seller's listings` mode (`startMode: "sameAuthor"`).** Fetches all listings from a specific seller — paste a listing ID or detail URL into the new `Listing ID or URL` input and the Actor resolves the seller internally (fetches the listing's detail to extract the user ID, then queries all their listings). This mirrors the website UX where "same seller" listings appear on a product's detail page. Results are paginated (48/page) and tagged with `attributes.slot = "sameAuthor"`. This is the same data as `Seller listings` mode but accessed through a listing reference rather than a store/profile URL — useful when you found a listing via search and want to browse the seller's other items.

### \[1.6] - 2026-08-30

#### Added

- **`Seller listings` mode (`startMode: "seller"`).** Scrapes every listing a specific seller has published — the same mode covers business stores (`/store/{id}/{slug}/` pages) and personal accounts (`/membre/{id}` pages), because from a consumer's point of view both are just "a seller". Paste the seller page URL straight from the address bar into the new `Seller` input and the Actor detects which kind of seller it is from the path; a bare numeric ID (e.g. `290`) also works and is tried as a store first, then as a member profile (store and user ID spaces overlap on this site, so the URL form is the unambiguous one). Store grids paginate through the same GraphQL search the storefront uses (`filter.storeId`, 48/page, shared filter inputs work); member grids run through the site's profile query (24/page). Pagination, `maxItems`, and `maxPages` work exactly as in search/category mode. Store-grid prices come back with the `priceUnit` value `UNIT` (price already in DZD); the price resolver has always treated unknown units as ×1, so `priceAmount` is correct.
- **`sellerMemberSince` output field.** When the seller account was created, ISO-8601 (assumed UTC — the site returns an unzoned `YYYY-MM-DD HH:MM:SS`). Populated on `seller`-mode listing rows for member accounts and on the member seller-info row; null everywhere else. This closes the gap noted when the field was dropped in 0.5 — the value simply wasn't exposed by any operation the Actor used back then; the profile query returns it.
- **Seller-info row.** Every `seller` run now emits one extra row (identified by `attributes.__kind === "seller"`, same partial-row convention as category-tree rows) describing the seller itself: display name, username/ID, profile URL, and for member accounts the follower and announcement counts. A new **Seller profile** dataset view shows these rows. When the seller is a store and `includeContactInfo: true`, the row also carries the store's **public** contact block — phones (with WhatsApp/Viber/Telegram flags), emails, social links, and address — exactly what the storefront page shows to every visitor. Unlike individual sellers' contact info, this is not gated behind the site's click-to-reveal endpoints; the toggle is honored as the user's "no contact data" preference.
- **Pagination tail guard for store grids.** The storefront paginator's `lastPage` can overshoot the real tail (observed `lastPage: 39` where data ended at page 20). The adapter now treats an empty page as "past the end" instead of chasing phantom pages.

#### Changed

- **`includeContactInfo` is now effective in `seller` mode** in addition to `directUrls` (see the seller-info row above — store contact block only; member profiles expose no contact info on the web). The startup warning no longer fires for seller runs; its text now names the two modes where the toggle does something.
- **Input schema descriptions** for `maxItems`, `maxPages`, `categorySlug`, and the contact-related dataset fields now name the new mode where relevant.

#### Notes

- **No dataset-shape migration.** Existing rows are unchanged except the new `sellerMemberSince` field (null everywhere it was previously absent). Seller-mode listing rows use the same organic-row shape as search/category, plus the optional seller-info row. Consumers filtering datasets by populated fields are unaffected.
- **Member profiles expose no contact info anywhere on the web** — the site directs buyers to its mobile app instead. The phone/email arrays for member seller rows and member listing rows are therefore always empty; only stores have a public contact block.

### \[1.5] - 2026-08-29

#### Added

- **Output schema (`output_schema.json`)**. The Actor now declares an explicit output schema so the Store/Console Output tab shows the dataset under a "Listings" heading with a description of the per-mode row shape and the three view choices, instead of the default "Output schema is not defined" placeholder. Required for Apify Store publication. The Actor writes only to the default dataset — no key-value store or web-server surfaces — so those schemas are intentionally not defined.

### \[1.4] - 2026-08-29

#### Removed

- **`filterConnected` input field.** The site-side filter this mapped to accepts a Boolean but produces no change in results — `paginatorInfo.lastPage` and row shape are byte-identical for `true`, `false`, `null`, and omitted values. The input was misleading: it accepted a value, did nothing, and was previously described as a "Site-specific flag exposed by the API. Leave unset unless you know you need it." Dropped from the input schema, the engine's `ActorInput` type, the adapter's filter builder, and the per-Actor main entry. Saved tasks that pass `filterConnected` will see a schema-validation warning; remove the field from the task input.

#### Changed

- **`filterOrigin` is now a 3-option select.** The site's `SearchFilterInput.origin` is a strict enum that accepts only `INDIVIDUAL` (personal sellers) or `STORE` (business / shops) and rejects any other string with `VALIDATION_INVALID_TYPE_VARIABLE`. The previous free-text `textfield` accepted any string and surfaced the server-side error for any value that wasn't one of those two enums. The UI now offers `Any` (default — omits the field, server returns both), `Individual sellers only`, or `Stores only`. The adapter forwards the raw enum value and lets the server be the source of truth.
- **`defaultMemoryMbytes` dropped from 256 to 128.** Observed peak across recent runs is 110 MB. 128 MB leaves ~16% headroom over the observed peak while halving the per-run compute-unit cost (the workload is single-threaded HTTP I/O, so the smaller CPU share is enough). The `min: 128` floor and `max: 2048` ceiling are unchanged for users who run huge `directUrls` lists or want extra headroom.

#### Notes

- **Both changes are breaking for any saved task or API caller that passes the old field name.** A saved task with `filterConnected: true` fails input validation with an "Unknown property" warning; a caller that passed a free-text `filterOrigin` value other than `INDIVIDUAL` / `STORE` still hits the same `VALIDATION_INVALID_TYPE_VARIABLE` they always did, but the new UI prevents it. The Actor is not yet published to the public Apify Store (still dev-only), so this is a clean cut.

### \[1.3] - 2026-08-29

#### Added

- **`Category` field accepts full category-page URLs.** Users used to copy-pasting from the address bar can now paste `https://www.ouedkniss.com/automobiles_vehicules/1` straight into the `Category` field; the adapter's new `normalizeCategorySlug` helper extracts the slug from the first path segment. Bare slugs (`automobiles_vehicules`) still work unchanged. URLs that point to non-category surfaces (store pages, member profiles, search results, the homepage, auth pages, etc.) are rejected up front with an actionable error message so we fail loud instead of silently returning an empty banner. Verified against `https://www.ouedkniss.com/automobiles_vehicules/1`, `https://www.ouedkniss.com/immobilier`, the bare-domain form `www.ouedkniss.com/voitures/2`, query strings, fragments, and nested URLs like `/automobiles_vehicules/voitures`. 11 vitest cases cover the parser (`packages/sites/tests/ouedkniss.test.ts`).
- **Two new dataset views** in `dataset_schema.json`:
  - **`Category menu`** — for rows from `Category menu (full tree)` mode (the `What to scrape` value `"categories"`). Shows the category name, slug, parent breadcrumb, depth, rank, icon, and delivery flag. Previously, `categories` runs left the dataset's Overview view showing only `id` and `scrapedAt` for every row because all the other columns were null — users had to switch to the All fields view to see anything meaningful.
  - **`Listing details`** — for rows from `Specific listing URLs` mode (the `What to scrape` value `"directUrls"`). Adds description, seller info, gallery count, video URL, contact info, and the specs map. The existing `Listings` view (formerly called `Overview`) stays the default card projection for organic and sponsored-listing rows.

#### Changed

- **Renamed every user-facing label to match the actual OuedKniss site vocabulary.** The previous labels were either generic ("Keyword search"), developer jargon ("TopAnnounList banner (paid placements)"), or used internals (the enum keys themselves) that don't map to anything a user browsing ouedkniss.com would recognize. The new labels are what the site itself shows:
  - `startMode` enum titles: `Keyword search` → **Search by keyword** · `Category browse` → **Browse a category** · `Direct listing URLs` → **Specific listing URLs** · `Category tree (recursive)` → **Category menu (full tree)** · `TopAnnounList banner (paid placements)` → **Sponsored listings (banner)**. "Sponsorisées" / "Publicité" is the actual term OuedKniss uses for the paid banner, so the new label keeps that connection.
  - `startMode` title (was `Start mode`): **What do you want to scrape?**
  - `searchQuery` title: **Search keyword**. `categorySlug` title: **Category** (the word "slug" is meaningless to a non-developer). `startUrls` title: **Listing URLs**. `locale` title: **Language**, with `enumTitles` for the three supported values.
  - `maxItems` title: **Maximum results**. `maxPages` title: **Maximum pages**. `maxDepth` title: **How deep to walk the menu**. `maxRequestsPerMinute` title: **Requests per minute**.
  - `includeContactInfo` title kept (descriptive). `dedupe` title: **Skip duplicate listings**. `proxyMaxAttempts` title: **Proxy fallback attempts** (dropped "probe").
  - Filter titles: `Filter: region (wilaya) IDs` → **Filter by wilaya (region)** · `Filter: city (commune) IDs` → **Filter by commune (city)** · `Filter: min price (DZD)` → **Minimum price (DZD)** · `Filter: max price (DZD)` → **Maximum price (DZD)** · `Filter: must have pictures` → **Only listings with photos** · `Filter: must have a price set` → **Only listings with a price** · `Filter: exchange accepted` → **Only listings that accept trade-in** · `Filter: delivery available` → **Only listings with delivery** · `Filter: connected (OuedKniss-specific flag)` → **Only 'Connected' listings (OuedKniss feature)** · `Filter: order by` → **Sort by** with `enumTitles` for the two orderings · `Filter: page size` → **Items per page**.
- **`Overview` dataset view renamed to `Listings`** and its description rewritten to point users at the `Category menu` view for category-tree runs (which now have a meaningful projection) and at the `Listing details` view for direct-URL runs that want the full enrichment.
- **`actor.json` description** mentions "sponsored-listing banner" instead of "paid cross-listing banner" so the storefront copy aligns with the new enum title.
- **README prose** uses the new labels throughout (mode table, input tables, field-by-field notes, Notes-and-limits section, FAQ). The **Tutorial** has a sentence about switching to the **Category menu** dataset view after a `Category menu` run, and a sentence about switching to the **Listing details** dataset view after a `Specific listing URLs` run with contact info on.
- **`How deep to walk the menu` default dropped from 5 to 2.** Recursive category-tree walks grow quickly — at depth 5 over OuedKniss's full menu the Actor pushes hundreds of nodes and many API calls, while most users only need the top 2-3 levels to discover a category slug. New default: `2`. Raise it explicitly if you need to walk deeper; the field's `maximum: 10` is unchanged. The engine-side fallback (`packages/engine/src/engine.ts`) and the per-Actor main entry (`actors/ouedkniss/src/main.ts`) were updated to match.

#### Notes

- **No dataset-shape migration path.** All five modes produce the same `Listing` row shape they did before — this is purely a UX change (labels + dataset view configuration). Consumers reading the JSON output are unaffected. The dataset view config changes from "Overview" only to "Listings / Category menu / Listing details"; users who script against a specific view name would need to update, but the previous `Overview` view's `transformation.fields` and `display.properties` were preserved under the new `Listings` name so the columns are byte-identical.
- **URL acceptance is opt-in via copy-paste.** The `Category` field's UI is still a plain text input, not a URL picker — the helper just makes pasting a URL into it work. Users who want to keep using bare slugs don't need to change anything.

### \[1.2] - 2026-08-28

#### Fixed

- **`includeContactInfo` was silently ignored outside `directUrls` mode.** The toggle only takes effect in the `op.op === "detail"` branch of `nextOps`, which is only seeded by `startMode: "directUrls"`. In `search`, `category`, `categories`, and `topAnnounList` modes the phone and email arrays were always empty regardless of the toggle — but the old docs only flagged `topAnnounList`, so users running `search`/`category` had no way to know the toggle was a no-op without reading the adapter code. The descriptions for the input field, the two seller-contact output fields, the marketing bullet, the input table, the field-by-field notes, the Notes-and-limits section, and the engine's `ActorInput` JSDoc now all say explicitly: **only effective in `directUrls` mode**. The Actor also prints a startup `WARN` line when the toggle is enabled outside `directUrls`, so a user who copy-pastes a config from a previous `directUrls` run into a `search`/`category` run sees the mismatch immediately instead of debugging empty phone arrays after the run completes.

### \[1.1] - 2026-08-28

#### Removed

- **`slot` field.** Every row from a single run has the same `slot` value (one run = one mode), so the field was constant noise. The discriminator moved out of the data — there's only one `Listing` shape now; which fields are populated tells you which mode produced the row (organic rows have `priceAmount`/`categoryPath`/`sellerId`, banner rows have `storeId`/`likeCount`, category-tree rows have `categoryName`/`categorySlug`/`parentTreeNames`).
- **`status` field.** Every row the API returns maps to `"active"` (the resolver returns `'unknown'` only when the API sends an unrecognized enum, which doesn't happen in practice). Kept in the engine type only for adapter-internal use; not surfaced in the dataset.
- **`categories` view** in the dataset schema. Filterable rows for category-tree data are still in the dataset (under the same shape as organic/banner rows), but the dedicated view was redundant with the Overview — Overview is now a complete view of every row, and a filter on `categorySlug !== null` (in the All fields view) is what splits out tree rows.
- **`ListingStatus` and `ListingSlot` types** from `@dz/engine`. Internal-only after the `slot`/`status` removal; no consumer needed them.

#### Changed

- **`categoryLeafId` renamed to `categoryId`.** The previous name's "Leaf" qualifier was confusing — the field holds the listing's category ID, period. `categoryId` is what consumers see on organic rows; the same value appears as `id` on rows produced by `startMode: "categories"`, so the rename makes cross-dataset joins obvious.
- **Breadcrumb arrays → breadcrumb strings.** `categoryPath` (organic row's full path root-to-leaf) and `parentTreeIds` / `parentTreeNames` / `parentTreeSlugs` (category-tree row's ancestors) are now single strings with the parts joined by `›` (U+203A) — the most common breadcrumb symbol across the web. Empty strings represent "no path" (topAnnounList rows for `categoryPath`; root-level nodes and non-category rows for `parentTree*`). The previous parallel-array form was a workaround for the dataset table view's poor array rendering; the flat-string form is cleaner in JSON and renders the breadcrumb inline in the Overview column.
- **New `primaryImageUrl` scalar.** First image URL, surfaced as its own scalar so the Apify dataset table renders it as an image thumbnail. The `images` array still carries the full gallery for programmatic use — but arrays render as "N items" badges in the table view, so the thumbnail needs a scalar. The category-tree `categoryIcon` field has worked this way since launch; `primaryImageUrl` brings organic/banner rows to parity.

#### Notes

- **No dataset-shape migration path.** Consumers reading the JSON output will see `slot` and `status` gone, `categoryLeafId` renamed, breadcrumb arrays replaced by strings, and a new `primaryImageUrl` field appear. The Actor is not yet published to the public Apify Store (27 dev runs so far), so a clean cut is fine. If you forked this schema for an external pipeline, search-and-replace `slot: "organic"` filters to `categoryPath !== ""` (or similar), `categoryLeafId` → `categoryId`, and unwrap the breadcrumb arrays (`parts = categoryPath.split(" › ")`).

### \[1.0] - 2026-08-28

#### Removed

- **`rootCategorySlug` input field.** Merged into `categorySlug` — see below. Any saved task or API caller passing `rootCategorySlug` must update the input key to `categorySlug`. The semantic ("start the category walk at this sub-tree") is preserved; only the field name changed.

#### Changed

- **`categorySlug` is now a single field that covers three modes.** In `category` mode it's the category to browse; in `topAnnounList` it scopes the banner; in `categories` it scopes the tree walk. The previous schema exposed `categorySlug` *and* `rootCategorySlug` as separate fields, which made users guess which one to fill for a given mode. They are now one field whose meaning depends on `startMode`. Field description in the input schema spells out the three modes explicitly. Ignored in `search` and `directUrls`. The internal `QueuedOperation.vars.categorySlug` was already a single name, so the engine did not need a code change.
- **Pagination framing generalized across search and category.** Search and category browse share the exact same `SearchAnnouncementsQuery` operation with `filter.page` + `paginatorInfo` (verified in `packages/sites/src/ouedkniss.ts:paginatorOf` and `profiles/ouedkniss.md`). The previous wording emphasized pagination as a category-only feature ("Category browse (with pagination)", "browse a category with pagination" in the description). The category-specific qualifier has been dropped everywhere; both rows in the README's mode table now mention pagination explicitly, and the input schema's `startMode.enumTitles` for the category mode is just "Category browse".

### \[0.9] - 2026-08-28

#### Changed

- **Memory defaults dropped from 1 GB to 256 MB.** Observed peak across every `startMode` is ~110 MB and CPU stays under 20%, so the platform's 1024 MB default was paying for four times more capacity than the workload needs. The Actor now declares `defaultMemoryMbytes: 256` with `min: 128` and `max: 2048`. Apify rounds the value to the nearest power of 2 either way; this just makes the default match the workload. The CPU share scales linearly (256 MB ≈ 1/16 core), but the workload is single-threaded HTTP I/O so the smaller core is enough. You can still raise memory per run if you push a huge `directUrls` list.
- **Description no longer claims "no proxy".** The earlier description said "no headless browser, no proxy"; the proxy requirement (OuedKniss's edge blocks the default datacenter egress) was already called out in the README and the user-visible note, but the description was stale. It now matches the README — five modes, proxy-agnostic phrasing — and no longer mentions "headless browser".

#### Notes

- **Run timeout is not part of `actor.json`.** Apify's `actor.json` schema only carries memory defaults; the run timeout lives on the Actor's Console settings and on each run invocation. The platform default for the `js-crawlee-cheerio` template is **300 s**, which is fine for the default caps (`maxItems: 200, maxPages: 20`) but too tight for large runs (each page is rate-limited to `maxRequestsPerMinute` requests/min, so `maxItems: 1000` ≈ 5 min of network alone, plus startup and proxy probing). For long runs, set a longer timeout when you start the run (UI "Timeout" field, or `timeoutSecs` in the API call). A good rule of thumb is `(maxPages / maxRequestsPerMinute) × 60 + 60` seconds, with a floor of 300 s.

### \[0.8] - 2026-08-28

#### Fixed

- **`categories` mode still failed every push with `Schema validation failed` after the 0.7 row-shape fix.** The site's GraphQL `Category.delivery` field is a Boolean (was being typed as `string | null` in the adapter, then `categoryDelivery: false` was getting rejected by the schema's `[string, null]` type union). `categoryDelivery` now types as `[boolean, null]` in both the schema and the dataset rows.

### \[0.7] - 2026-08-28

#### Fixed

- **`categories` mode failed every push with `Schema validation failed`.** The dataset schema's `required` list (`url`, `title`, `scrapedAt`) didn't match what the category-tree walker emitted — those rows are tree nodes, not listings, so they don't have a `url` or a `title`. Every request retried 4× before failing, so a single `categories` run produced zero output on Apify.

#### Added

- **`slot: "category"` discriminator.** Category-tree nodes from `startMode: "categories"` now use the same row shape as organic/topAnnounList listings, with `slot` set to `"category"`. Filter the dataset with `slot === "category"` to keep only the tree.
- **Category-mode fields on every row (null/empty outside category mode):** `categoryName`, `categorySlug`, `categoryIcon`, `categoryDelivery`, `categoryRank`, `categoryDepth`, and three parallel `parentTreeIds`/`parentTreeNames`/`parentTreeSlugs` arrays for the breadcrumb from root to (excluding) the current node.
- **A `Categories` dataset view.** Table projection tuned for the tree walk: name, slug, rank, depth, parent path.

#### Changed

- **Required fields on the dataset.** `required` is now `["slot", "id", "scrapedAt"]` (was `["slot", "id", "url", "title", "scrapedAt"]`). Every row has these; everything else is per-slot.

### \[0.6] - 2026-08-28

#### Removed

- **`sponsored` field from every output row.** The field was `true` for ~100% of rows because the site returns the same value for organic and paid listings. The field added no information; it was just misleading noise.
- **`includeTopAnnounList` input.** The sponsored/organic split is gone — see "Added" below.
- **`maxPromotedItems` input.** No longer needed — see "Added" below.
- **`maxOrganicItems` input.** Replaced by `maxItems`. Almost everything this Actor pushes is organic, so the per-mode cap is now `maxItems`. Default raised from `0` to `200` for visibility.

#### Added

- **`topAnnounList` is now its own `startMode`.** The cross-listing paid banner used to be an opt-in add-on (`includeTopAnnounList: true`) bolted onto `search` / `category` runs. Banner entries have a much sparser response than organic listings (only title, price, store metadata, and one image), so they're now a dedicated mode. Run with `{ "startMode": "topAnnounList" }`. Pass `categorySlug` to scope the banner to a category.
- **`slot` discriminator on every output row.** Either `"organic"` (search / category / directUrls / categories results) or `"topAnnounList"` (banner entries). Filter the dataset into its streams.
- **`storeId`, `storeName`, `storeImageUrl`, `storeProfileUrl`, `likeCount` fields.** Populated only for `topAnnounList` rows; `null` for organic ones.

#### Changed

- **`maxItems` is now the standard cap.** `maxOrganicItems` (formerly the per-mode cap) is gone.
- **`startMode` enum** now includes `"topAnnounList"`.

### \[0.5] - 2026-08-28

#### Added

- **`proxyMaxAttempts` input field.** How many proxy IPs to try at the start of a run when the site blocks the actor's exit IP. The first one that returns data is reused for the rest of the run. Default 5. No effect on local runs.
- **Run log shows proxy outcome.** `Network: proxy=on` is the success signal; the `proxy=off` variants cover the known failure modes.

#### Removed

- **`sourceSite` field from every output row.** Was always the constant `"ouedkniss"`.
- **`price.currency` field from every output row.** Was always `"DZD"`.

#### Changed

- **Flat output schema.** Every top-level field is now a scalar or a list of scalars (the only intentional exception is `attributes`, which holds free-form category-specific specs). The `seller.phones` array is exposed as five parallel scalar arrays (`sellerPhoneNumbers`, `sellerPhoneIntls`, `sellerPhoneWhatsapps`, `sellerPhoneVibers`, `sellerPhoneTelegrams`) — index N pairs across them.
- **Flat filter input.** The previous `filters: { regionIds, priceRange, ... }` object is replaced with top-level scalar fields prefixed `filter*` (`filterRegionIds`, `filterPriceMin`, `filterHasPictures`, …).
- **`startUrls` is now `string[]`** instead of `Array<{ url, method }>`. Each item is just a detail-page URL.
- **Dropped redundant output fields.** `price.raw` (derivable from `priceAmount`), `location.raw` (derivable from wilaya/commune/street), `media.imageCount` (derivable from `images.length`), `media.videoThumbnail` (only useful with video), `seller.memberSince` (always `null`), `seller.messengerLink` (derivable from phones + WhatsApp), `category.leafLabel` (last item of `categoryPath`).

### \[0.4] - 2026-08-27

#### Fixed

- **Every request to OuedKniss from Apify returned 200 with a 0-byte body.** The site blocks the default datacenter egress IP range. All API calls now route through Apify Proxy.

#### Changed

- **Local runs skip proxy creation entirely.** Previously the Actor tried to create a proxy configuration even when running locally; now it only does so on real Apify containers.

### \[0.3] - 2026-08-27

#### Fixed

- **Search returned zero results when filter booleans defaulted to `false`.** The `SearchFilterInput.exchange` and `SearchFilterInput.delivery` fields only accept `null` (or omission) — the server returns `VALIDATION_INVALID_TYPE_VARIABLE` for any other value, including `false`. The adapter now treats `false` the same as "unset" so the form's default of `false` no longer breaks every run.

#### Changed

- **Version format dropped the patch segment.** Apify's UI displays the first two segments only (`M.m`), so `0.2.0` was being shown as a malformed version. Bumped to the two-segment form `0.3`.

### \[0.2.0] - 2026-08-26

#### Changed

- **Migrated to monorepo layout.** This Actor is now `actors/ouedkniss/` in the dz-ecommerce-scrapers monorepo, importing the shared engine from `@dz/engine` and the per-site adapter from `@dz/sites`. Behavior is unchanged.
- **Two independent output streams.** `maxItems` and `maxPromotedItems` are independent caps for the organic and promoted streams. `maxItems` still works as a global fallback when both are unset.

#### Added

- Per-actor README (`actors/ouedkniss/.actor/README.md`) with Tutorial, FAQ, Pricing sections — rendered on the Actor's Apify Console and Store pages.

### \[0.1.0] - 2026-08-25

#### Added

- Initial release. Four run modes: `search`, `category`, `directUrls`, `categories`.
- Optional contact enrichment (`includeContactInfo`) — no proxy required.
- Optional promoted-banner scraping (`includeTopAnnounList`).
- Unified `Listing` output schema, union-compatible with future site Actors in the same project.
