Skip to content
Back to skills

Sf Import

ASecurity

Use when: kullanıcı "screaming frog import", "SF csv yükle", "sf-export al", "site crawl ingest", "yeni SF batch geldi" der ya da bir projenin sf-exports/{date}/raw/ dizinine yeni CSV'ler düştüğünde manager bu skill'i tetikler. Master.xlsx'in 6 SF-türevi sheet'ine (crawl_sitemap, redirect_404, schema, on_page_audit, tech_seo, robots_txt) atomic write yapar; raw CSV'leri inbox/sf/ altına envelope-li JSON olarak persist eder. Also use when: pilot project sf-exports/{export_date}/raw/ dizini haz...

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

Works with

  • cli
  • mcp

Security analysis

A100/100

Scanned October 2, 2026

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

Installs into .claude/skills of the current project.

Are you the author of Sf Import?

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

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

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: sf-import
description: |
  Use when: kullanıcı "screaming frog import", "SF csv yükle", "sf-export al",
  "site crawl ingest", "yeni SF batch geldi" der ya da bir projenin
  sf-exports/{date}/raw/ dizinine yeni CSV'ler düştüğünde manager bu
  skill'i tetikler. Master.xlsx'in 6 SF-türevi sheet'ine (crawl_sitemap,
  redirect_404, schema, on_page_audit, tech_seo, robots_txt) atomic write
  yapar; raw CSV'leri inbox/sf/ altına envelope-li JSON olarak persist
  eder.
  Also use when: pilot project sf-exports/{export_date}/raw/ dizini hazır;
  Tier 1 14/14 zorunlu rapor mevcut; Tier 2 search_console_all eksik AMBER
  warning üretilmesi gerek; sf_imported provenance event yazılacak. (Opsiyonel
  mcp__gsc__list_sitemaps sitemap cross-check Phase-X'e ertelendi — core ingest
  GSC'siz, sadece disk CSV'leriyle çalışır.)
  Do not use when: GSC (gsc-pull), DataForSEO (dfs-pull), Scrapling
  (scrapling-ops) verisi geliyor — ayrı ingestion skill'leri. Master.xlsx
  yokken çağırma; init-project önce çalışmalı (DURUR #6). Tier 1 < 14
  ise FAIL et, fallback YASAK (DURUR #2).
version: "1.0"
status: active
category: ingestion
inputs:
  project_slug:
    type: string
    required: true
    description: "Slug; resolves projects/{slug}/sf-exports/ + master.xlsx."
  sf_export_path:
    type: string
    required: true
    description: "SF export dizini path (mutlak veya workspace-relative). raw/ subfolder zorunlu."
  source_run_id:
    type: string
    required: false
    description: "v1.8 Phase 3 D-SF-07: optional upstream run_id from sf-crawl-orchestrator. When present, chained into the provenance event so drift-check can correlate the orchestrator MCP crawl with this file-based ingest. Body 8-step protocol UNCHANGED."
outputs:
  - "master.xlsx#crawl_sitemap"
  - "master.xlsx#redirect_404"
  - "master.xlsx#schema"
  - "master.xlsx#on_page_audit"
  - "master.xlsx#tech_seo"
  - "master.xlsx#robots_txt"
  - "events.jsonl"
  - "inbox/sf/{date}-{slug}.json"
consumes:
  - "init-project:projects/{slug}/master.xlsx"
produces:
  - "quick-wins"
  - "drift-check"
triggers:
  manual: []
  natural_language: |
    "screaming frog import", "SF csv yükle", "sf-export al",
    "site crawl ingest", "yeni SF batch geldi"
  hooks: []
mcp_tools:
  # sf-import's deterministic core (scripts/ingestion/sf_import.py) reads SF
  # CSVs from disk and writes Excel — it invokes NO MCP tool. The GSC sitemap
  # tools power the OPTIONAL sitemap cross-check (Step 5), which is Phase-X
  # deferred (not wired into the shipped implementation), so they are optional.
  required: []
  optional:
    - "mcp__gsc__list_sitemaps"
    - "mcp__gsc__get_sitemap"
budget:
  uses_paid_mcp: false
  estimated_credits: 0
autonomy:
  confidence: HIGH
  requires_approval: false
  safe_auto_execute: true
---

# sf-import — ingestion skill (Phase 5 Wave 2)

Deterministic CLI (`scripts/ingestion/sf_import.py`), **not** a
`workflow_runner` run shell. The stages below run in a single `main(argv)`
pass: each DURUR is a `print("ERROR: … (DURUR #N)", file=sys.stderr)` plus
a distinct non-zero **exit code** (no `{run_id}.json` state file, no
`workflow_runner.create_run`/`complete`, no string-typed `outputs`). Raw
CSV envelope discipline is mandatory: every SF batch is described in a
single envelope-JSON dropped into `inbox/sf/` *before* any per-sheet
projection runs, so a projection bug never costs us the upstream
filename → tier mapping.

The 6 SF-derived sheets are written via **`transaction.replace`** (an
idempotent refresh — clears each sheet's data block, then re-lands the
rows), so re-running a batch *refreshes* rather than duplicates. The
tier-validation + envelope-inbox + per-sheet-write shape is shared with
the other ingestion skills, but the write mechanism differs per skill
(gsc-pull appends; dfs-pull is staging-only). Deviate only with an ADR.

## Inputs (frontmatter contract)

| Name              | Type   | Default | Notes                                                              |
|-------------------|--------|---------|--------------------------------------------------------------------|
| `project_slug`    | string | —       | Required. Resolves `projects/{slug}/master.xlsx`.                  |
| `sf_export_path`  | string | —       | Required. Path to a SF export dir; `raw/` subfolder enforced.      |

`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#crawl_sitemap` — sitemap-vs-crawl summary rows.
- `projects/{slug}/master.xlsx#redirect_404` — 404 + redirect chain rows.
- `projects/{slug}/master.xlsx#schema` — structured-data audit rows.
- `projects/{slug}/master.xlsx#on_page_audit` — title/meta/h1 coverage rows.
- `projects/{slug}/master.xlsx#tech_seo` — tech issue summary rows.
- `projects/{slug}/master.xlsx#robots_txt` — robots/directive issue rows.
- `projects/{slug}/_state/events.jsonl` — provenance entries (`source.kind=sf_csv`).
- `projects/{slug}/inbox/sf/{date}-{slug}.json` — envelope JSON listing every
  CSV (filename, tier, row_count, file_hash) for drift recovery.

## CLI protocol (6 stages, one `main()` pass)

> Invocation (manager, or `sf-crawl-orchestrator` subprocess):
>
> ```bash
> python3 -m scripts.ingestion.sf_import \
>     --project <slug> \
>     --sf-export-path projects/<slug>/sf-exports/<date>/ \
>     [--workspace-root <root>]  [--dry-run]
> ```
>
> `--workspace-root` defaults to `$PSEO_WORKSPACE_ROOT`. There are no other
> flags — in particular `--source-run-id` does NOT exist (the `source_run_id`
> *frontmatter* input carries provenance correlation, not a CLI flag).

### Stage A — workspace + path resolution (DURUR #5)

`main(argv)` resolves `--workspace-root` (or `$PSEO_WORKSPACE_ROOT`). Unset →
`print("ERROR: … required (DURUR #5)", file=sys.stderr)` and `return 5`. A
relative `--sf-export-path` is joined onto `workspace_root`.

### Stage B — `validate_export_path` (DURUR #1)

```python
raw_dir = sf_root / "raw" if (sf_root / "raw").is_dir() else sf_root
if not raw_dir.is_dir():
    print(f"ERROR: raw_dir not found at {sf_root} or {sf_root}/raw (DURUR #1)",
          file=sys.stderr)
    return 1
```

### Stage C — `validate_tier1` (sf-required-reports.schema Tier 1 14/14, DURUR #2)

`match_tiers` lives in **`scripts.ingestion.sf_import`** itself (there is no
separate `sf_validate` module). It walks `raw/`, normalizes filenames (lower,
strip `de_` / `v_` / `p_` / `bi_` prefixes, strip `(N)` / `-copy` / `_old`
suffixes, fold Turkish `ı→i`), and matches each canonical_name against
`schemas/sf-required-reports.schema.json` `definitions.canonicalName`. Tier 1
missing → RED FAIL (exit 2). Tier 2 missing → AMBER warn, NOT a fail
(`search_console_all` is the canonical Tier 2 exemption surfaced in pilot
validation).

```python
matched, missing_t1, missing_t2 = match_tiers(raw_dir)   # defined in this module
if missing_t1:
    print(f"ERROR: Tier 1 missing: {sorted(missing_t1)} (DURUR #2)", file=sys.stderr)
    return 2
amber: list[str] = []
if missing_t2:
    amber.append(f"Tier 2 missing (AMBER): {sorted(missing_t2)}")
```

### Stage D — `envelope_inbox` (raw → drift-recoverable JSON, DURUR #6)

First confirm the write target exists; a missing `master.xlsx` → DURUR #6
(exit 6 — `init-project` must have run). Then the `build_envelope` helper
writes a single envelope JSON to `inbox/sf/{date}-{slug}.json` BEFORE any
projection runs — for every matched CSV it captures sha256, row_count,
filename_original + filename_normalized + tier. The envelope is the durable
witness that a given SF batch was observed even if the Excel write fails
downstream.

```python
master_xlsx = workspace_root / "projects" / args.project / "master.xlsx"
if not master_xlsx.exists():
    print(f"ERROR: master.xlsx missing at {master_xlsx} (DURUR #6)", file=sys.stderr)
    return 6
inbox_path = build_envelope(args.project, sf_root, raw_dir, matched, amber, workspace_root)
```

Envelope shape (written by `build_envelope`):

```json
{
  "_meta": { "captured_at": "…Z", "tool": "screaming_frog",
             "project_slug": "<slug>", "sf_export_path": "…",
             "raw_dir": "…", "amber_warnings": ["Tier 2 missing (AMBER): …"] },
  "files": [
    { "canonical_name": "internal_all", "tier": "required",
      "filename_original": "internal_all.csv", "filename_normalized": "internal_all.csv",
      "file_hash": "sha256:…", "row_count": 192 }
  ]
}
```

### Stage E — `sitemap_xcheck` — **Phase-X DEFERRED (not wired)**

> **Status: deferred capability, not part of the shipped contract.** The
> intended cross-check — compare the SF-discovered URL list against the GSC
> submitted sitemaps and log discrepancies into `crawl_sitemap` as a
> diagnostic — is NOT implemented. `main()` goes straight from the envelope
> (Stage D) to the projection write (Stage F); it never calls
> `mcp__gsc__list_sitemaps`, and there is **no `sf_sitemap_xcheck` module**.
> The GSC sitemap tools are therefore `optional` in the frontmatter, and
> DURUR #4 is a deferred-condition placeholder (see DURUR conditions below).
> When this lands (a future Wave) it belongs at the manager/caller layer —
> sf-import's core stays GSC-free, disk-CSV only.

### Stage F — `write_excel` (per-sheet, idempotent replace)

Six `transaction.replace` calls — one per sheet — over the projections
returned by `sf_projection.project_all(raw_dir)`. `replace` (NOT `append`)
clears each sheet's data block then re-lands the rows, so **re-importing the
same batch refreshes rather than duplicates** (idempotent). Each call goes
through the single approved write path with backup, lock, schema validation,
and post-write `tool_computed` provenance emission. The 6 sheets and their
CSV → row projections (inline mapping; the formal `staging-to-excel-map.json`
arrives in Phase 6):

| Logical sheet     | Source CSV(s)                                      | Row shape (master-excel.schema)                                 |
|-------------------|----------------------------------------------------|------------------------------------------------------------------|
| `crawl_sitemap`   | `internal_all`, `sitemaps_all`, `crawl_depth`      | category / metric / value / status / action                       |
| `redirect_404`    | `response_codes_all`, `redirect_chains`            | url / inlinks / action / target_url / status                      |
| `schema`          | `structured_data_all`                              | schema_type / status / location / scope / remaining_work          |
| `on_page_audit`   | `page_titles_all`, `meta_description_all`, `h1_all`| url / target_query / impressions_30d / clicks_30d / in_title /... |
| `tech_seo`        | `issues_overview_report`, `directives_all`         | issue_category / detail / affected_urls / impact / resolution / ... |
| `robots_txt`      | `directives_all`, `indexability`                   | id / level / issue / detail / resolution                          |

In `sf_import.py` this is the `project_and_write` helper:

```python
from scripts.excel import transaction
from scripts.ingestion import sf_projection
projections = sf_projection.project_all(raw_dir)
counts: dict[str, int] = {}
for sheet, rows in projections.items():
    result = transaction.replace(
        master_xlsx, sheet, rows, project_slug, writer="sf-import",
    )
    counts[sheet] = result.rows_affected
```

`transaction.replace` itself emits a `tool_computed` provenance event per
write. Stage F then supplements that with a single `sf_csv` source provenance
record (below) so the data lineage is `sf_csv → tool_computed`. **Idempotency:**
a re-run with the same CSVs replaces the prior rows in place — no duplicate
rows accumulate across re-imports.

### Stage F (cont.) — `sf_csv` source provenance

A single `sf_csv` source-provenance event records the lineage. `source_folder`
is the raw dir relative to the workspace root (via `_rel_to_workspace`, which
falls back to an absolute string for an out-of-tree path rather than crashing);
`row_count` and `rows_written` are the total rows projected across the 6 sheets.
The `run_id` is auto-allocated race-free under the append flock (P1-11).

```python
from scripts.state import events_writer
total_rows = sum(counts.values())
events_writer.append_provenance(
    project_id=project_slug,
    source={
        "kind": "sf_csv",
        "source_folder": _rel_to_workspace(Path(raw_dir), workspace_root),
        "row_count": total_rows,
    },
    operation="ingest",
    rows_written=total_rows,
    workspace_root=workspace_root,
)
```

### Stage G — completion (stdout summary + exit 0)

There is **no `workflow_runner.complete`** and **no string-typed `outputs`
dict** — `main()` prints a human-readable summary to stdout and returns 0 on
success. `--dry-run` stops after the envelope (Stage D), before any Excel write.

```python
print(f"OK: matched {len(matched)} files (Tier1 {t1}/14, Tier2 {t2}/10)")
print(f"Envelope: {inbox_path}")
for w in amber:
    print(f"WARN: {w}")
if args.dry_run:
    print("DRY-RUN: skipping Excel projection.")
    return 0
counts = project_and_write(raw_dir, master_xlsx, args.project,
                           workspace_root=workspace_root)
for sheet, n in counts.items():
    print(f"OK: {sheet} ← {n} rows")
return 0
```

**Exit codes** — the DURUR signalling channel (there is no run-state file):

| Outcome | Exit |
|---------|------|
| Success / `--dry-run` | 0 |
| DURUR #1 — no `raw/` subfolder | 1 |
| DURUR #2 — Tier 1 < 14/14 | 2 |
| DURUR #3 — `RowSchemaError` during projection | non-zero (uncaught traceback) |
| DURUR #5 — no `--workspace-root` / `$PSEO_WORKSPACE_ROOT` | 5 |
| DURUR #6 — no `master.xlsx` | 6 |

## Filename normalization (Tier 1 matching)

The pilot project export ships every Tier 1 file twice: once with the
canonical name (`internal_all.csv`) and once with a `de_` prefix (German
locale shadow `de_internal_all.csv`). Plus a third Turkish-i variant
(`de_ınternal_all.csv`). Normalization rules, applied in order:

1. Lowercase the basename.
2. Drop extension (`.csv`).
3. Strip leading locale shadow prefix `de_` (defensive — pilot artifact).
4. Strip leading export-mode prefix `v_` / `p_` / `bi_` (SF visualization /
   page / bulk-import mode).
5. Fold Turkish dotless `ı` → ASCII `i` (Turkish-locale filename collision).
6. Drop `(N)` and `-copy` / `_old` version suffixes.

The first matching canonical_name (per `sf-required-reports.schema`)
wins. Ambiguous matches (two canonical names matching the same file)
abort the import — no silent picks.

## Tier policy

| Tier        | Missing → | Pilot exemption                                  |
|-------------|-----------|--------------------------------------------------|
| Required    | RED FAIL  | none (all 14 enforced)                           |
| Recommended | AMBER     | `search_console_all` (pilot ships 9/10, OK)      |
| Optional    | SILENT    | full set silent                                  |

## DURUR conditions (6)

Stop and flag the manager — do not patch, do not fall back. Each is a
`print(… DURUR #N)` + a distinct exit code (DURUR #3 surfaces as an uncaught
`RowSchemaError` traceback).

1. `sf_export_path` missing or no readable `raw/` subfolder. → exit 1
2. Tier 1 < 14/14 — RED FAIL, do not proceed to projection. → exit 2
3. `transaction.replace` raises `RowSchemaError` for any sheet — the exception
   is NOT swallowed, so the batch aborts with a non-zero (traceback) exit;
   `replace` schema-validates BEFORE clearing, so a rejected sheet is left
   untouched.
4. **(Phase-X deferred, not wired)** — `mcp__gsc__list_sitemaps` sitemap
   cross-check is not implemented in `sf_import.py`; the GSC tools are
   `optional`. When the cross-check lands, a `list_sitemaps` failure becomes
   the DURUR #4 hard stop. Today this condition never fires.
5. `PSEO_WORKSPACE_ROOT` env unset and no `--workspace-root` arg. → exit 5
6. `master.xlsx` missing under `projects/{slug}/` — `init-project`
   must have run first. → exit 6

## Cross-references

- Schemas: `schemas/sf-required-reports.schema.json` (Tier 1 14, Tier 2
  10, canonicalName enum), `schemas/sf-export-mapping.schema.json`
  (filename_alias normalization), `schemas/master-excel.schema.json`
  (6 sheet column structures + statusEnum + severityEnum),
  `schemas/events.schema.json` (`source.kind=sf_csv`),
  `schemas/gsc-tool-mapping.schema.json` (`gsc__list_sitemaps` enum — used by
  the deferred Stage E cross-check, not the shipped core),
  `schemas/skill-frontmatter.schema.json` (this frontmatter).
- Cross-modules (IMPORT-only): `scripts/excel/transaction.py` (`replace`),
  `scripts/ingestion/sf_projection.py` (`project_all`),
  `scripts/state/events_writer.py` (`append_provenance`). **NOT**
  `workflow_runner` — sf-import is a plain CLI, not a run shell.
- Tests: `tests/skills/test_sf_import.py` (10 cases: 6-criterion acceptance
  gate incl. live SF batch + `source_run_id` handoff pair + frontmatter
  validation + required-tools parity).
- Pilot data path: `projects/{slug}/sf-exports/{export_date}/raw/`
  (Tier 1 14/14 + Tier 2 9/10 with `search_console_all` AMBER).

## Discipline checklist

- [x] TODO/fallback YASAK — every DURUR raises, none silently downgrade.
- [x] Schema-first — frontmatter validates against
      `schemas/skill-frontmatter.schema.json` Draft 7.
- [x] Plugin-agnostik — no slug literals; `project_slug` flows through.
- [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` / `sf_projection` /
      `events_writer` are imported, never modified from this skill.
- [x] F1: write target is `master.xlsx` (lowercase, schema-shaped). The
      legacy `demo-dental_MASTER.xlsx` (Turkish-emoji) is DOKUNULMAZ.
- [x] Idempotent writes — `transaction.replace` refreshes each sheet's data
      block (re-import does not duplicate); DURUR signalling is exit-code
      based (no `workflow_runner` run shell, no string-typed `outputs`).

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.