Skip to content
Back to skills

Cannibalization

ASecurity

Use when: kullanıcı "cannibalization", "kannibalizasyon", "keyword conflict", "aynı keyword'e iki sayfa", "URL çakışması", "anahtar kelime çakışması", "iki sayfam aynı sorguya çıkıyor", "intent split" der ya da /pseo-cannibalization çağırır. Also use when: aktif projenin GSC verisi mevcut; aynı sorguda ≥2 URL ranking ediyor; quick-win / content-decay ile birlikte triage; F-08 invariant kontrolünden sonra cannibalization sheet doldurulacak. Do not use when: tek sayfanın decay analizi (content-...

  • 27 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 2, 2026
ai-agentspythongoshellbashperformance

Works with

  • terminal
  • cli
  • mcp

Security analysis

A100/100

Scanned October 2, 2026

npx -y skills add popiliadam/platinum-seo-engine --skill cannibalization --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Cannibalization?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Cannibalization
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/popiliadam-cannibalization/badge)](https://www.skillsdirectory.com/skills/popiliadam-cannibalization)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: cannibalization
description: |
  Use when: kullanıcı "cannibalization", "kannibalizasyon", "keyword
  conflict", "aynı keyword'e iki sayfa", "URL çakışması", "anahtar kelime
  çakışması", "iki sayfam aynı sorguya çıkıyor", "intent split" der ya
  da /pseo-cannibalization çağırır.
  Also use when: aktif projenin GSC verisi mevcut; aynı sorguda ≥2 URL
  ranking ediyor; quick-win / content-decay ile birlikte triage; F-08
  invariant kontrolünden sonra cannibalization sheet doldurulacak.
  Do not use when: tek sayfanın decay analizi (content-decay), pozisyon
  11-20 fırsat taraması (quick-wins), tech audit (tech-audit), yeni
  içerik planı (new-content-plan) — ayrı discovery skill'leri. Master.xlsx
  yokken çağırma; init-project önce çalışmalı (DURUR #6).
version: "1.0"
status: active
category: discovery
inputs:
  project_slug:
    type: string
    required: true
    description: "Slug; resolves projects/{slug}/master.xlsx + project.config.json."
  days_back:
    type: integer
    required: false
    default: 28
    description: "GSC date window end=today, start=today-N."
  min_impressions:
    type: integer
    required: false
    default: 10
    description: "K threshold per page; pages below K are dropped before grouping."
  default_status:
    type: string
    required: false
    default: "TODO"
    description: "statusEnum seed value for new conflict rows."
outputs:
  - "master.xlsx#cannibalization"
  - "outputs/reports/{date}-cannibalization.md"
  - "events.jsonl"
  - "inbox/gsc/{date}-search_analytics-cannibalization-{slug}.json"
consumes:
  - "init-project:projects/{slug}/master.xlsx"
  - "gsc-pull:master.xlsx#gsc_performance"
produces:
  - "drift-check"
  - "monthly-report"
triggers:
  manual: ["/pseo-cannibalization"]
  natural_language: |
    "cannibalization", "kannibalizasyon", "keyword conflict", "URL
    çakışması", "aynı keyword'e iki sayfa", "anahtar kelime çakışması",
    "iki sayfam aynı sorguya çıkıyor", "intent split"
  hooks: []
  scheduled:
    - cron: "0 9 * * 2"
      mode: "report-only"
mcp_tools:
  required:
    - "mcp__gsc__search_analytics"
  optional:
    - "mcp__gsc__enhanced_search_analytics"
budget:
  uses_paid_mcp: false
  estimated_credits: 0
autonomy:
  confidence: HIGH
  requires_approval: true
  safe_auto_execute: false
---

# cannibalization — discovery skill (Phase 7 Wave 1)

10-step protocol. Steps map 1:1 to `workflow_runner` invocations + the
spec §16.5 8-step MCP discipline. Raw JSON drift recovery is mandatory:
the MCP response is dropped into `inbox/gsc/` *before* any transform
runs, so a transform bug never costs us the upstream payload.

This skill follows the **convention authority** established by
`skills/discovery/quick-wins/SKILL.md` (Phase 5) and
`skills/ingestion/gsc-pull/SKILL.md` (Phase 6). The 10-step protocol
shape, raw JSON inbox discipline, URL normalization (D-03), DURUR + flag
rule, and provenance event format are reused verbatim — only the domain
content (target sheet, transform script, conflict heuristic) changes.
Deviate only with an ADR.

## Inputs (frontmatter contract)

| Name              | Type    | Default | Notes                                                  |
|-------------------|---------|---------|--------------------------------------------------------|
| `project_slug`    | string  | —       | Required. Resolves `projects/{slug}/master.xlsx`.       |
| `days_back`       | integer | 28      | GSC date window end=today, start=today-N.               |
| `min_impressions` | integer | 10      | K threshold per page; below-K pages dropped before grouping. |
| `default_status`  | string  | "TODO"  | statusEnum seed for new rows (master-excel.schema definitions). |

`workspace_root` is resolved via `PSEO_WORKSPACE_ROOT` env or explicit
test override (mirrors workflow_runner / events_writer).

## Outputs (artifacts produced)

- `projects/{slug}/master.xlsx#cannibalization` — one row per query
  conflict (7 cols, schema-locked).
- `projects/{slug}/outputs/reports/{date}-cannibalization.md` —
  human-readable summary (top conflicts, primary URLs, resolution mix).
- `projects/{slug}/_state/events.jsonl` — `event_kind=provenance`
  entries (`source.kind=gsc_mcp`, `target_excel_sheet=cannibalization`).
- `projects/{slug}/inbox/gsc/{date}-search_analytics-cannibalization-{slug}.json`
  — raw MCP payload (drift recovery).

## 10-Step Body Protocol

> Each step name must match the `steps[*].name` passed to
> `workflow_runner.create_run`. Names are stable identifiers across runs.

### Step 1 — `create_run`

Open a workflow run shell. The state file lives at
`projects/{slug}/_state/workflows/{run_id}.json` (ADR-021, ADR-019).

```python
from scripts.state import workflow_runner
handle = workflow_runner.create_run(
    skill="cannibalization",
    project_slug=project_slug,
    steps=[
        {"name": "fetch_search_analytics"},
        {"name": "transform"},
        {"name": "request_approval"},
        {"name": "write_excel"},
        {"name": "render_report"},
    ],
)
```

### Step 2 — `fetch_search_analytics` (MCP §16.5 step 3 — raw inbox first)

Pull `query` × `page` rows over the recent window so groups can form.

```python
workflow_runner.start_step(handle.run_id, 0,
                           project_slug=project_slug)
raw = mcp__gsc__search_analytics(
    siteUrl=project_config["gsc"]["site_url"],
    startDate=(today - days_back).isoformat(),
    endDate=today.isoformat(),
    dimensions=["query", "page"],
)
inbox_path = (
    workspace_root / "projects" / project_slug
    / "inbox" / "gsc"
    / f"{today.isoformat()}-search_analytics-cannibalization-{project_slug}.json"
)
inbox_path.parent.mkdir(parents=True, exist_ok=True)
inbox_path.write_text(json.dumps(raw, ensure_ascii=False, indent=2))
workflow_runner.finish_step(handle.run_id, 0,
                            project_slug=project_slug,
                            output_ref=str(inbox_path))
```

### Step 3 — `transform`

Pure compute via `scripts/discovery/cannibalization_transform.py`:

```bash
python3 scripts/discovery/cannibalization_transform.py \
    --raw inbox/gsc/{date}-search_analytics-cannibalization-{slug}.json \
    --min-impressions 10 \
    --default-status TODO \
    --output-dir _state/transform/{run_id}/
```

Produces a JSON array (`cannibalization`) shaped to the master-excel
schema (7 columns: conflict_pair, overlapping_queries_est, total_impact,
resolution, note, status, priority). URL normalization (D-03) is applied
here, not at fetch time, so the raw inbox copy is byte-faithful to the
MCP response. The transform is idempotent: same input → byte-identical
output.

If `meta.conflict_count == 0`, the skill SKIPS write_excel and emits a
"no cannibalization detected" notice (DURUR #7 — clean exit, not error).

### Step 4 — `request_approval` (skill EXIT awaiting_approval)

```python
workflow_runner.request_approval(
    handle.run_id, project_slug=project_slug,
    approver="user",
    subject=f"{len(rows)} cannibalization conflict bulundu, master.xlsx#cannibalization'a yazalım mı?",
    step_index=2,
)
# Skill exits here. The user replies in a fresh session; resume below.
```

### Step 5 — Resume (`approve` → continue)

```python
workflow_runner.approve(handle.run_id, project_slug=project_slug,
                        approver="user")
```

### Step 6 — `write_excel` (atomic, schema-validated)

Single `committer.commit` call for the cannibalization sheet — the
orchestrator's idempotent commit path (whole-block `transaction.replace`
from the schema's `data_start_row`, so re-running never duplicates rows on
the `cannibalization` snapshot sheet). Goes through the single approved write
path with backup, lock, schema validation, and post-write provenance event
emission.

```python
from scripts.orchestration import committer
committer.commit(
    workspace_root/"projects"/project_slug/"master.xlsx",
    "cannibalization",
    cannibalization_rows,
    run_id=handle.run_id,
    project_slug=project_slug,
    writer="cannibalization",
)
```

### Step 7 — `render_report`

`render_template.py templates/reports/cannibalization.template.md
data.json` → `outputs/reports/{date}-cannibalization.md`. Variables:
`$project_slug`, `$date`, `$days_back`, `$conflict_count`,
`$top_conflict_pair`, `$top_total_impact`, `$top_resolution`,
`$report_summary`.

### Step 8 — Provenance event

```python
from scripts.state import events_writer
events_writer.append_provenance(
    project_id=project_slug,
    source={"kind": "gsc_mcp", "mcp_server": "gsc",
            "mcp_tool": "gsc__search_analytics",
            "response_bytes": len(json.dumps(raw))},
    operation="project_excel",
    target_excel_sheet="cannibalization",
    rows_written=len(cannibalization_rows),
)
```

### Step 9 — `complete`

```python
workflow_runner.complete(handle.run_id, project_slug=project_slug, outputs={
    # F5: outputs.* must be STRING-TYPED
    "conflict_count": str(len(cannibalization_rows)),
    "report_path": str(report_path),
    "raw_json": str(inbox_path),
})
```

### Step 10 — Done

Workflow status flips `running → done` (workflow-run schema) and a
`workflow_action=done` event lands in `events.jsonl` (ADR-020).

## URL normalization (D-03 invariant)

Every URL passing through this skill is normalized via
`scripts.discovery.cannibalization_transform.normalize_url`. The function
is **idempotent**: `normalize_url(normalize_url(u)) == normalize_url(u)`.
Rules: lowercase scheme+host, IDN→punycode, strip default ports, strip
trailing slash (root excluded), drop fragment, drop tracking params,
sort remaining query keys.

The transform self-checks idempotency on every emitted URL — a drift
between transform and downstream consumers is escalated as DURUR rather
than silently masked.

## Cannibalization detection logic (I2 contract)

A query with ≥2 distinct URLs above K is only a **candidate**; it becomes a
**conflict** only when **ALL THREE** signals hold (so single-keyword
volatility and brand SERPs are not mislabelled as cannibalization):

```
1. Parse rows: each entry contributes (query_lower, url_normalized,
   clicks, impressions, position).
2. Drop rows with impressions < min_impressions (default 10).
3. Group by query_lower; collapse same (query, url) pairs by summing;
   position is impression-weighted.
4. Candidate = query with ≥2 distinct URLs after the K filter.
5. Candidate → CONFLICT only when ALL hold:
     (a) NON-BRAND query — brand tokens (from project.config brand/domain,
         passed as --brand-token / brand_tokens) exclude brand-dominated
         queries.
     (b) CLICK-SHARE DILUTION — no single URL holds > 70% of the query's
         clicks (a query with zero clicks has no traffic to dilute → not a
         conflict).
     (c) COMPETITION SIGNAL — top-URL flip-flop between the two most recent
         windows (needs the optional --previous window) OR ≥2 URLs
         simultaneously in positions 1-20 with spread ≤ 5.
6. For each conflict emit one row:
     conflict_pair             "{query} :: {url1} | {url2} | ..."  (sorted)
     overlapping_queries_est   #conflict queries with the same URL set
     total_impact              "{sum_clicks} clicks"
     resolution                ALWAYS "differentiate intent / adjust
                               internal-link hierarchy" (default)
     note                      "primary URL: {top}; signal: {...};
                               consolidate (301) only if intent overlap
                               confirmed — operator review"
     status                    statusEnum (default "TODO")
     priority                  P1 (>=100) / P2 (>=20) / P3 otherwise
7. Sort rows by total_impact desc, then by conflict_pair for determinism.
   meta records brand_excluded / share_excluded / signal_excluded so a
   non-empty input that yields zero conflicts is explainable.
```

### Resolution policy (no auto-consolidate)

The default recommendation is **"differentiate intent / adjust internal-link
hierarchy"**. A **301-consolidate is NEVER auto-recommended** — duplicate
intent must be explicitly confirmed by a human, and consolidation is ALWAYS
operator-reviewed (the note carries this caveat). This replaces the old
position-spread heuristic ("consolidate to top URL" / "intent split
investigation" / "topical merge candidate"), which over-flagged
consolidation off rank spread alone.

### F-15 stays AMBER-by-design

A non-empty `cannibalization` sheet correctly drives the F-15 drift check to
**AMBER** (not GREEN, not RED) — cannibalization conflicts are
production-ready findings that require operator triage, so AMBER is the
intended terminal state. Do NOT delete rows or force GREEN to clear F-15
(see `feedback_f15_amber_terminal`).

Optional CLI flags wiring the new signals (Step 3 transform):
`--previous inbox/gsc/{prior-date}-search_analytics-cannibalization-{slug}.json`
(top-URL flip-flop) and `--brand-token <token>` (repeatable; from
project.config brand/domain — engine stays agnostic).

## DURUR conditions (8)

Stop and flag the manager — do not patch, do not fall back.

1. `mcp__gsc__search_analytics` returns auth/network/scope error or an
   empty/malformed payload. STOP, do not write to master.xlsx.
2. Raw JSON inbox path cannot be created (workspace path missing or
   non-writable, e.g. `PSEO_WORKSPACE_ROOT` misconfigured). STOP,
   request manager check.
3. `master.xlsx#cannibalization` column count or names don't match
   schema (`schemas/master-excel.schema.json#cannibalization`). STOP,
   schema-first violation.
4. `transaction.append` raises `RowSchemaError` (e.g.,
   `overlapping_queries_est` not int, `status` not in statusEnum). STOP.
5. `workflow_runner.create_run` fails schema validation
   (`schemas/workflow-run.schema.json`). STOP.
6. `PSEO_WORKSPACE_ROOT` env var unset and no explicit `workspace_root`
   arg passed to `workflow_runner` / `events_writer`. STOP, surface to
   manager.
7. Transform reports `meta.conflict_count == 0` (no candidate query
   survived the (a)/(b)/(c) predicate — either none had ≥2 URLs above K,
   or all were brand/share/signal-excluded; see meta.*_excluded). NOT an
   error — clean exit with a "no cannibalization detected" notice; skill
   skips write_excel + render_report and goes straight to `complete` with
   `conflict_count="0"`.
8. URL normalization output drifts (idempotency check fails inside
   `cannibalization_transform.transform`). STOP, D-03 invariant broken.

## Cross-references

- Schemas: `schemas/master-excel.schema.json`
  (cannibalization sheet, 7 required_columns + `#/definitions/statusEnum`),
  `schemas/events.schema.json` (`source.kind=gsc_mcp`,
  `target_excel_sheet=cannibalization`),
  `schemas/gsc-tool-mapping.schema.json` (D-03 invariant),
  `schemas/skill-frontmatter.schema.json` (this frontmatter).
- Cross-modules (IMPORT-only): `scripts/state/workflow_runner.py`,
  `scripts/excel/transaction.py`, `scripts/state/events_writer.py`,
  `scripts/reporting/render_template.py`.
- Transform: `scripts/discovery/cannibalization_transform.py`.
- Tests: `tests/skills/test_cannibalization.py` (≥6 cases incl. smoke).
- Template: `templates/reports/cannibalization.template.md`.

## Discipline checklist

- [x] TODO/fallback YASAK — every DURUR raises, none silently downgrade
      (exception: DURUR #7 is a clean no-op exit, not a fallback).
- [x] Schema-first — frontmatter validates against
      `schemas/skill-frontmatter.schema.json` Draft 7.
- [x] Plugin-agnostik — no slug literals; `project_slug` flows through
      every path; transform has 0 hardcoded slug words.
- [x] ADR-013: `Use when`/`Also use when`/`Do not use when` are STRING
      content inside `description`, not separate fields.
- [x] Cross-module IMPORT discipline — `transaction` /
      `workflow_runner` / `events_writer` are imported, never modified
      from this skill. Transform has 0 imports from
      `scripts.excel.transaction`.
- [x] F1: write target is `master.xlsx` (lowercase, schema-shaped).
- [x] F5: `outputs.*` values are STRING-TYPED (artifact paths or
      stringified counts), never raw ints.
- [x] Append-only state — `events.jsonl` only grows; no in-place rewrite.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.