# Catalog Guard (`seeker_jp/catalog-guard`) Actor

- **URL**: https://apify.com/seeker\_jp/catalog-guard.md
- **Developed by:** [Koh Yamamoto](https://apify.com/seeker_jp) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.05 / catalog audit

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Catalog Guard

**Read-only supplier CSV change audits for AI agents and purchasing workflows.**

Compare two supplier catalogs before an import or price update. Get structured evidence for price changes, missing rows, additions, changed unit/currency/tax context, and ambiguous identifiers. No store credentials, web scraping or LLM API key required.

Version 0.1.0 is a working local tool with an official MCP SDK server and an Apify Actor adapter. It has **not been published to a marketplace**. Paid demand, cloud deployment, platform approval and live billing remain unverified. All examples are synthetic.

### What makes this specific

- Explicit UTF-8, UTF-8 BOM, CP932 (Windows Japanese) and Shift-JIS decoding. No silent encoding guessing.
- Exact decimal arithmetic. Full-width prices and JPY markers are supported; leading zeros in product keys are preserved.
- Unit, pack-size, currency and tax-status changes suppress numeric price comparisons **when those columns are configured**.
- Duplicate/empty keys, normalization collisions and malformed files block the comparison rather than produce misleading matches.
- Physical CSV line numbers and source SHA-256 hashes provide traceable evidence, including multiline quoted cells.
- Local CLI, MCP stdio and hosted Actor share the same deterministic core. No per-call LLM expense.

This is not a generic PDF summarizer or a Shopify importer. It audits supplied CSVs; it never modifies a catalog.

### Quick start

Python 3.11+; Python 3.12 recommended. Run from the project directory using your chosen environment:

```sh
python -m pip install .
python -m catalog_guard --input examples/request.json
```

The synthetic example returns **1 added, 1 missing, 3 changed and 1 unchanged row**:

- `0001`: 100 → 130, a 30% increase.
- `0002`: 1,200 per box → 250 per piece. **No percentage is calculated**, because the unit changed.
- `0003`: `800` → `８００`, format-only change, not a price increase.
- `0004`: absent from the new snapshot. This is **not proof of discontinuation**.
- `0005`: unchanged.
- `0006`: newly present.

CLI exit codes are intentional: **0 unchanged; 1 review; 2 blocked; 3 CLI/file error**. A nonzero review result is not an application crash.

#### Compare local Japanese files

```sh
python -m catalog_guard --before examples/before.csv --after examples/after.csv --config examples/config.json --json audit.json --markdown audit.md
```

Use `--before-encoding cp932` or `--after-encoding shift_jis` for those file bytes. Each side may use a different encoding. Report files must not already exist; the CLI refuses overwrites. Source files are only opened for reading.

#### Input contract

```json
{
  "before": {"csv": "SKU,Price,Unit,Currency\n001,100,piece,JPY\n"},
  "after": {"csv": "SKU,Price,Unit,Currency\n001,130,piece,JPY\n"},
  "keyColumns": ["SKU"],
  "priceColumns": ["Price"],
  "contextColumns": ["Unit", "Currency"],
  "priceChangeThresholdPercent": 20
}
```

Each source accepts exactly one of:

- `{"csv": "decoded Unicode text"}`
- `{"base64": "encoded original file bytes", "encoding": "cp932"}`

No paths, URLs, callbacks, code or credentials are accepted by the core/MCP/Actor. The CLI is the only interface that opens caller-specified local paths.

| Field | Default | Meaning |
| --- | --- | --- |
| `before`, `after` | required | Source objects above |
| `keyColumns` | required | One or more exact header names; composite keys supported |
| `priceColumns` | `[]` | Columns to parse as decimal amounts |
| `contextColumns` | `[]` | Configure every unit, currency, pack-size and tax-status column needed for comparability |
| `ignoreColumns` | `[]` | Explicitly excluded cell comparisons; columns must exist in both files |
| `priceChangeThresholdPercent` | `20` | Alert when absolute percentage change meets/exceeds this value; smaller changes are still reported |
| `maxRows` | `10000` | Hard input limit per file; maximum `20000` |
| `maxChanges` | `200` | Returned change details, maximum `1000`; counts remain complete |
| `delimiter` | `,` | Comma, tab or semicolon, explicitly chosen |
| `allowEmptyAfter` | `false` | Explicitly permit an empty new snapshot |

Column roles cannot overlap. Header names and keys are not automatically renamed, trimmed, lowercased or converted to numbers. Width/whitespace key changes are warnings and do not cause automatic identity merging. Duplicate or normalization-colliding keys within a snapshot block the audit.

#### Output contract

- `status`: `unchanged`, `review`, or `blocked`. Never interpret `unchanged` as authorization to update a store.
- `comparisonPerformed`: false when reliable matching was impossible; `summary` is then empty and `changes` is empty.
- `sources`: byte count, decoding method, row counts, headers and SHA-256 of original bytes (or UTF-8 representation for text inputs).
- `summary`: complete added/removed/changed/unchanged counts and per-flag row counts. A row may have several flags.
- `changes`: row keys, before/after physical line ranges, changed cells and numeric interpretation. Added/missing rows include their non-ignored values.
- `issues`: parser/configuration errors and warnings, with evidence where available.
- `schemaChanges`: added/removed headers. Only shared, non-ignored columns receive cell comparison.
- `changesTruncated`, `issuesTruncated`: always check these before assuming the detail lists are exhaustive.

`removed` means **missing from the supplied new file**, not proven deleted from a shop or discontinued by a supplier. A partial export can produce this result. Structural failures block the whole comparison; detail limits never turn an input into a sampled audit.

#### Price rules

- Accepts plain decimal numbers, correctly grouped thousands commas, NFKC full-width digits and `¥`, `JPY` or `円` markers.
- Does not accept guessed decimal-comma formats, scientific notation, non-JPY currency symbols, negative prices or embedded tax notes. Use a separate currency/tax column with a plain numeric amount.
- Empty prices are unknown, never zero, and generate warnings.
- Numeric price comparisons are suppressed for changed/blank configured context or changed explicit JPY markers. Unconfigured context cannot be checked; supply all relevant columns.
- Zero baseline has no percentage change. Decimal deltas are exact; displayed percentages round to four decimal places with decimal half-even rounding. Threshold decisions use the unrounded percentage.
- No currency conversion, tax calculation, pack-size conversion, fuzzy matching, semantic aliasing or procurement recommendation.

#### Bounds

Each input: 2,000,000 bytes, 100 columns, 200-character headers, 8,000-character cells, at most 20,000 data rows. Returned changes also have a 60,000-character JSON detail budget. Issues have a 100-item / 20,000-character budget. Entire entries may be omitted rather than silently shortening raw evidence; truncation flags and full counts expose this. These are not pagination APIs. Split snapshots into complete, matching key partitions upstream if exhaustive large reports are required.

### MCP connection

```sh
python -m pip install '.[mcp]'
python -m catalog_guard.mcp_server
```

The second command starts a stdio server and waits for a client; silence is normal. Tool name: **`audit_supplier_catalog`**. Arguments: `{"request": <the input object above>}`. Both structured JSON and text content are returned. The tool is annotated read-only and closed-world.

Configure a stdio-capable MCP host with your **absolute environment Python executable** and arguments `['-m', 'catalog_guard.mcp_server']`. An example host configuration is in `examples/mcp.config.example.json`; replace the executable placeholder. No host settings have been modified by this project. No public HTTP listener is provided.

The server uses official `mcp==2.1.1`. It implements a tool for agents; it does not run an autonomous agent, watch files, market itself or generate sales on its own.

### Apify Actor

`.actor/actor.json`, `.actor/input_schema.json`, `Dockerfile` and `catalog_guard.actor` form the hosted adapter. It accepts the same input object directly, pushes one report to the default dataset, and writes `OUTPUT` JSON plus `REPORT` Markdown to the run's default key-value store.

The adapter uses `apify==4.0.2`. A completed comparison invokes the custom **`catalog-audit`** event once through the SDK's save-and-charge operation. Blocked input does not invoke this custom event. When a charge cannot fit the run budget, no extra report copy is exported. Platform pricing configuration, synthetic events and live charging must be verified before any paid release; no price is activated by these source files.

See `docs/PUBLISHING.md` for the explicit release gates. The hosted route sends inputs and outputs to Apify storage; it is **not local-only**. Use local CLI/MCP when those files must stay on your machine.

### Privacy and security

Use only files you are authorized to process. Do not include unnecessary customer or personal data. The core makes no external calls and writes no files. Local MCP receives caller-provided content but cannot browse the filesystem. The CLI writes only explicitly named new reports. The hosted adapter persists input/output in the invoking run's storage according to Apify's configuration and retention; it does not promise automatic deletion.

CSV cells are untrusted data and may contain prompt injection, spreadsheet formulas or markup. The tool never evaluates them. Markdown output escapes source markup; JSON retains original values. Consumers must not execute cell content or paste formula-like strings into a spreadsheet without their own escaping. The prefix warning is advisory, not a complete spreadsheet security scanner.

### Development and verification

```sh
python -m pip install '.[mcp,actor,dev]'
python -m pytest -q
python -m ruff check src tests
python -m ruff format --check src tests
```

Tests include exact-price arithmetic, Japanese bytes, adversarial input, bounds, source-overwrite protection, real MCP in-memory/stdio calls, real Apify local-storage execution, and mocked billing/failed-storage paths. Live cloud billing and Docker execution are separate release gates. See `docs/VALIDATION.md` for recorded results.

### Scope and ownership

This is an independently implemented v0.1 product experiment, not a clone, official Shopify integration, financial adviser or automatic purchasing system. No third-party source code or commercial datasets were copied. Marketplace publication and any public source license require an explicit owner decision. An LLM may explain the report, but should not change its computed evidence.

# Actor input Schema

## `before` (type: `object`):

Object with csv text OR base64 file bytes and encoding (utf-8, utf-8-sig, cp932, shift\_jis). Maximum 2 MB decoded bytes.

## `after` (type: `object`):

Same format as before. An empty new catalog is blocked unless allowEmptyAfter is explicitly true.

## `keyColumns` (type: `array`):

Exact header names. Composite keys supported; no trimming or numeric coercion.

## `priceColumns` (type: `array`):

Decimal prices only; ambiguous formats block the audit.

## `contextColumns` (type: `array`):

Include unit, currency, pack size and tax status when present. Any change suppresses numeric deltas for that row.

## `ignoreColumns` (type: `array`):

Explicit columns to exclude from change comparison. Must exist in both inputs.

## `priceChangeThresholdPercent` (type: `number`):

Flag absolute changes greater than or equal to this percentage. All other changes remain in the report.

## `maxRows` (type: `integer`):

Exceeded limits block the whole audit, never silently sample it.

## `maxChanges` (type: `integer`):

Summary counts remain complete even if details are truncated.

## `delimiter` (type: `string`):

Explicit delimiter; no guessing.

## `allowEmptyAfter` (type: `boolean`):

Enable only if the empty new catalog is intentional. Missing rows are not proof of discontinued products.

## Actor input object example

```json
{
  "before": {
    "csv": "SKU,Price,Unit,Currency\n0001,100,each,JPY\n0002,1200,box of 6,JPY\n"
  },
  "after": {
    "csv": "SKU,Price,Unit,Currency\n0001,130,each,JPY\n0002,250,each,JPY\n"
  },
  "keyColumns": [
    "SKU"
  ],
  "priceColumns": [
    "Price"
  ],
  "contextColumns": [
    "Unit",
    "Currency"
  ],
  "priceChangeThresholdPercent": 20,
  "maxRows": 10000,
  "maxChanges": 200,
  "delimiter": ",",
  "allowEmptyAfter": false
}
```

# Actor output Schema

## `results` (type: `string`):

The structured audit result stored as one item in the default dataset.

## `jsonReport` (type: `string`):

The same structured result stored under the OUTPUT record for integrations.

## `markdownReport` (type: `string`):

A human-readable review report. It is evidence only and must not be auto-applied.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "before": {
        "csv": "SKU,Price,Unit,Currency\n0001,100,each,JPY\n0002,1200,box of 6,JPY\n"
    },
    "after": {
        "csv": "SKU,Price,Unit,Currency\n0001,130,each,JPY\n0002,250,each,JPY\n"
    },
    "keyColumns": [
        "SKU"
    ],
    "priceColumns": [
        "Price"
    ],
    "contextColumns": [
        "Unit",
        "Currency"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("seeker_jp/catalog-guard").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "before": { "csv": """SKU,Price,Unit,Currency
0001,100,each,JPY
0002,1200,box of 6,JPY
""" },
    "after": { "csv": """SKU,Price,Unit,Currency
0001,130,each,JPY
0002,250,each,JPY
""" },
    "keyColumns": ["SKU"],
    "priceColumns": ["Price"],
    "contextColumns": [
        "Unit",
        "Currency",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("seeker_jp/catalog-guard").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "before": {
    "csv": "SKU,Price,Unit,Currency\\n0001,100,each,JPY\\n0002,1200,box of 6,JPY\\n"
  },
  "after": {
    "csv": "SKU,Price,Unit,Currency\\n0001,130,each,JPY\\n0002,250,each,JPY\\n"
  },
  "keyColumns": [
    "SKU"
  ],
  "priceColumns": [
    "Price"
  ],
  "contextColumns": [
    "Unit",
    "Currency"
  ]
}' |
apify call seeker_jp/catalog-guard --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,seeker_jp/catalog-guard"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/EJnToLeJD2bJedhx7/builds/JR4MA3dudM2bk8a1Z/openapi.json
