Gbp audit
Skill popiliadam/platinum-seo-engine/skills/discovery/gbp-audit
Use when: kullanıcı "GBP audit", "Google Business Profile kontrol", "place_id eksik mi", "business listing kontrol", "harita listing audit", "google haritalar profili", "GMB audit" der ya da `/pseo-gbp-audit` çağırır. DFS HEAVY ingestion (paid MCP, ~3 credit per audit); budget pre-flight ZORUNLU. Local-service profile gate şarttır — profilde "local-service" yoksa skip (Step 1 early exit). Also use when: project.config.profiles içinde 'local-service' var; init-project çalışmış (master.xlsx hazır); aktif projenin GBP listing durumu için gap analysis (NAP / categories / photos / hours / attributes / posts / Q&A / reviews 8 category triage) gerekiyor; bulgular master.xlsx#gbp_audit'a schema-locked 7-column formatta yazılacak (statusEnum + severityEnum codebase definition reuse). Do not use when: profile != local-service (DURUR #6 skip); master.xlsx eksikse init-project önce çalışmalı (DURUR #2); budget exhausted (DURUR #1, awaiting_approval); GSC veri ingestion (gsc-pull), tech audit (tech-audit), keyword research (dfs-pull), content decay analizi (content-decay) gerekiyorsa — her birinin ayrı komutu vardır. Otonom GBP API submit YASAK (feedback_indexing_api_consent): bu skill sadece audit + rapor üretir, GBP dashboard'una hiçbir şey yazmaz.From its SKILL.md
npx -y skills add popiliadam/platinum-seo-engine --skill gbp-auditAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
21.1 KB, as published. Nobody here has run it
gbp-audit — discovery skill (Phase 5 Wave 1, G-AI-02 closure)
8-step protocol. Steps map 1:1 to workflow_runner invocations + the
spec §16.5 MCP discipline + §16.8 budget pre-flight + ADR-016 cost
tracking. Single paid MCP call (business_data_business_listings_ search, ~3 credits per run); Scrapling fallback fires only when DFS
returns empty (e.g. business has no claimed GBP listing yet, in which
case the audit emits a HIGH-severity "GBP listing not found" gap row
and the operator action is to claim/create the listing).
Profile gate is the headline difference from tech-audit. Before
any paid call, Step 1 reads project.config.profiles and exits early
(status=skipped) if "local-service" is not in the list. e-commerce
or b2b-saas pure plays do not need GBP optimization; running this
skill against them wastes credits and pollutes
master.xlsx[gbp_audit] with irrelevant rows.
Hard constraint: read-only audit. Per
feedback_indexing_api_consent, this skill does NOT submit anything
to the GBP API, GMB dashboard, or any other Google submission
endpoint. It only ingests existing listing data, identifies gaps, and
emits a report. The operator decides what to fix manually.
Inputs (frontmatter contract)
| Name | Type | Default | Notes |
|---|---|---|---|
project_slug | string | — | Required. Resolves projects/{slug}/master.xlsx. |
nap_source | string | local/nap.json | Optional. Canonical NAP doc, project-relative (schemas/local-nap.schema.json, R-144). |
workspace_root is resolved via PSEO_WORKSPACE_ROOT env or explicit
test override (mirrors workflow_runner / events_writer / tech-audit).
Outputs (artifacts produced)
projects/{slug}/master.xlsx#gbp_audit— schema-locked 7 cols (audit_id | audit_date | category | gap_description | severity | recommended_action | status).severityis severityEnum (CRITICAL/HIGH/MEDIUM/LOW);statusis statusEnum (TODO seed at emission; operator transitions via done_protocol).projects/{slug}/outputs/reports/{date}-gbp-audit.md— human-readable summary (per-category gap count + severity histogram + recommended next actions).projects/{slug}/_state/events.jsonl—event_kind=provenanceentry (source.kind=dataforseo_mcp,target_excel_sheet=gbp_audit,cost.creditspopulated per ADR-016).projects/{slug}/inbox/dfs/{date}-gbp-listing-{slug}.json— raw business_listings_search payload (drift recovery; paid response).
8-Step Body Protocol
Step names match
steps[*].namepassed toworkflow_runner.create_run. Names are stable identifiers across runs.
Step 1 — profile_gate (early-exit before budget pre-flight)
The skill MUST read project.config.profiles FIRST. If "local- service" is not in the array, the workflow exits immediately with
status=skipped and emits an event_kind=audit event noting the
skip reason. NO paid MCP call, NO Excel write.
config = json.loads((project_dir / "project.config.json").read_text())
profiles = config.get("profiles", [])
if "local-service" not in profiles:
return {"status": "skipped",
"reason": "Profile 'local-service' missing from project.profiles",
"project_slug": project_slug}
DURUR #6 trigger: profile_gate not satisfied → early exit (this is
a graceful skip, not an error; the manager surface treats it as
"audit not applicable").
Step 2 — preflight_budget (§16.8 + ADR-016, MANDATORY)
Identical contract to tech-audit Step 1 but with a lower credit estimate (single endpoint, conservative MAX ≈ 3 credits):
estimate = 3 # business_data_business_listings_search MAX
envelope = gbp_audit_transform.preflight_budget(
estimated_credits=estimate,
project_config_path=project_root / "project.config.json",
events_path=project_root / "_state" / "events.jsonl",
)
If envelope["exceeded"] → BudgetExceededError → workflow routed
to awaiting_approval per spec §10 (DURUR #1). NEVER silently
downgrade or skip the check.
Step 3 — create_run
Open a workflow run shell. State file at
projects/{slug}/_state/workflows/{run_id}.json (ADR-021).
handle = workflow_runner.create_run(
skill="gbp-audit",
project_slug=project_slug,
steps=[
{"name": "profile_gate"},
{"name": "preflight_budget"},
{"name": "fetch_listing"},
{"name": "scrapling_fallback"}, # may no-op
{"name": "transform"},
{"name": "request_approval"},
{"name": "write_excel"},
{"name": "render_report"},
],
)
Step 4 — fetch_listing (MCP §16.5 — raw inbox first)
Identity inputs come from the canonical NAP doc (rules/local-seo-
discipline.md R-144), NOT from brand_identity — that block is a
VISUAL-identity contract (project-config.schema.json#brand_identity,
additionalProperties: false) and has never carried
business_name/primary_location keys; the pre-v1.1 contract here was
a phantom that could not validate against any live config.
nap_path = project_dir / nap_source # default "local/nap.json"
canonical_nap = (
json.loads(nap_path.read_text("utf-8")) if nap_path.exists() else None
) # corrupt file => json.JSONDecodeError propagates (DURUR #9 fail-loud)
business_name = (canonical_nap or {}).get("business_name") or config["display_name"]
location_hint = ((canonical_nap or {}).get("address") or {}).get("city", "")
raw = mcp__dataforseo__business_data_business_listings_search(
business_name=business_name,
location=location_hint,
limit=1,
)
inbox_path = (
workspace_root / "projects" / project_slug
/ "inbox" / "dfs"
/ f"{today.isoformat()}-gbp-listing-{project_slug}.json"
)
inbox_path.parent.mkdir(parents=True, exist_ok=True)
inbox_path.write_text(json.dumps(raw, ensure_ascii=False, indent=2))
events_writer.append_provenance(
project_id=project_slug,
source={"kind": "dataforseo_mcp", "mcp_server": "dataforseo",
"mcp_tool": "dataforseo__business_data_business_listings_search",
"response_bytes": len(json.dumps(raw))},
operation="ingest",
target_excel_sheet=None, # ingest: no Excel write yet
rows_written=0,
cost={"provider": "dataforseo",
"credits": 3.0,
"budget_key": "project.config.dataforseo.budget_credits_per_day"},
)
Auth/networking failure → DURUR #3 (parallel to tech-audit DURUR #2).
Step 5 — scrapling_fallback (conditional — DFS empty result)
if not listing_found(raw):
# DFS returned no result. The business may have an unclaimed listing
# on Google Maps but not surfaced via the listings_search endpoint.
# Fall back to Scrapling against the Google Maps place page.
scraped = mcp__ScraplingServer__fetch(
url=f"https://www.google.com/maps/search/{quote(business_name)}",
wait=2,
)
# Parsing scraped HTML is best-effort; if it also yields nothing,
# we proceed to the transform with listing=None and the gap analysis
# emits a HIGH-severity "GBP listing not found" row.
If Scrapling itself fails (403, anti-bot block) → not a DURUR; the
transform handles listing=None by emitting the "listing not found"
gap row. Anti-bot blocks are a known Scrapling failure mode and do
not warrant blocking the audit.
Step 6 — transform (pure compute, gap analysis + severity matrix)
The transform is invoked as a Python function, not a CLI — the
module exposes no __main__. run() is the single public entrypoint;
it performs the profile gate, budget pre-flight, DFS fetch + Scrapling
fallback (the MCP/budget boundary is stubbed at module level for the
production orchestrator to wire), then the pure-compute per-category
gap analysis (_analyze_gaps):
from scripts.discovery import gbp_audit_transform
result = gbp_audit_transform.run(
project_slug=project_slug,
workspace_root=workspace_root, # or resolved via PSEO_WORKSPACE_ROOT
)
# result["status"] ∈ {"skipped", "awaiting_approval", "success"}
# on success: result["gap_rows"] — schema-locked 7-col gbp_audit rows
The gap analysis consumes the fetched listing dict directly and applies
the per-category severity matrix. The NAP branch (v1.1, GAP-A2 — the
matrix declared it since v1.7 Phase 5 without an implementation)
compares listing name/phone/address against the canonical
local/nap.json via scripts.discovery.nap_consistency.compare_nap
(Turkish-aware folding, E.164 phone normalization, abbreviation-
expanded address tokens, multi-location resolution by location_id).
The listing-not-found path stays comparison-free — without a listing
there is nothing to compare, and the not-found row is already the
blocking finding:
| Category | Gap | Severity | Recommended action |
|---|---|---|---|
| nap | listing not found | HIGH | Create or claim GBP listing |
| nap | NAP mismatch vs local/nap.json (one row per mismatched field) | HIGH | Sync the field across GBP + site footer + LocalBusiness JSON-LD to match the canonical (R-144) |
| nap | canonical NAP file missing | MEDIUM | Create local/nap.json per schemas/local-nap.schema.json, re-run |
| categories | primary missing | HIGH | Set primary category in GBP dashboard |
| categories | < 2 secondary | MEDIUM | Add at least 2 relevant secondary categories |
| photos | < 3 photos | HIGH | Add ≥ 3 high-quality photos |
| photos | < 10 photos | MEDIUM | Add photos to reach 10+ for healthy listing |
| hours | regular hours missing | HIGH | Add regular business hours |
| hours | holiday hours missing | MEDIUM | Add upcoming holiday hours |
| attributes | profile-relevant attr empty | MEDIUM | Fill profile-relevant GBP attributes |
| posts | none last 30d | LOW | Publish ≥ 1 GBP post (update / offer / event) |
| qa | no Q&A engagement | LOW | Seed Q&A with FAQ-aligned questions |
| reviews | response rate < 50 % | MEDIUM | Respond to outstanding reviews |
| reviews | avg rating < 4.0 | MEDIUM | Address negative-review themes via service ops |
Each row is one master.xlsx[gbp_audit] entry with:
audit_id=gbp-<uuid8>(the transform owns ID generation)audit_date= today UTC ISO 8601 date portionstatus=TODO(seed; operator transitions via done_protocol)
Step 7 — request_approval (skill EXIT awaiting_approval)
workflow_runner.request_approval(
handle.run_id, project_slug=project_slug,
approver="user",
subject=f"{len(rows)} GBP gap row tespit edildi, master.xlsx#"
f"gbp_audit'a yazalım mı? (HIGH={n_high}, MEDIUM={n_med}, "
f"LOW={n_low})",
step_index=6,
)
# Skill exits here. Operator reviews report + resumes via /pseo-status.
Step 8 — write_excel + render_report + final provenance event
On resume (operator approves):
transaction.append(
workbook_path=workspace_root / "projects" / project_slug / "master.xlsx",
sheet="gbp_audit",
rows=gbp_audit_rows,
project_slug=project_slug,
writer="gbp-audit",
)
render_template(
"templates/reports/gbp-audit.template.md",
data={"rows": gbp_audit_rows, "config": config, "today": today},
out=workspace_root / "projects" / project_slug
/ "outputs" / "reports" / f"{today.isoformat()}-gbp-audit.md",
)
events_writer.append_provenance(
project_id=project_slug,
source={"kind": "dataforseo_mcp", "mcp_server": "dataforseo",
"mcp_tool": "dataforseo__business_data_business_listings_search",
"response_bytes": len(json.dumps(raw))},
operation="project_excel",
target_excel_sheet="gbp_audit",
rows_written=len(gbp_audit_rows),
cost={"provider": "dataforseo",
"credits": 3.0,
"budget_key": "project.config.dataforseo.budget_credits_per_day"},
)
workflow_runner.complete(handle.run_id, project_slug=project_slug, outputs={
# F5: outputs.* must be STRING-TYPED.
"gbp_audit_rows": str(len(gbp_audit_rows)),
"high_count": str(n_high),
"medium_count": str(n_med),
"low_count": str(n_low),
"credits_used": "3",
"report_path": str(report_path),
"raw_json": str(inbox_path),
})
DURUR conditions (9)
Stop and flag the manager — do not patch, do not fall back.
- Budget pre-flight FAIL (
check_budget.pyexit 1 OR projected_usedbudget) →
BudgetExceededError→awaiting_approvalper spec §10. NEVER silently downgrade. (Step 2) master.xlsxmissing or schema drift → init-project re-run.mcp__dataforseo__business_data_business_listings_searchreturns auth / network / scope error → STOP. (Step 4)- DFS response schema drift (envelope shape unrecognized after both
adapter passes) →
BusinessListingsSchemaDriftError. transaction.appendraisesRowSchemaError(severityEnum violation, statusEnum violation, column drift). (Step 8)- Profile gate skip —
"local-service"not inproject.config.profiles→status=skipped, no paid call, no Excel write. (Step 1; this is a graceful exit, not an error.) workflow_runner.create_runfails schema validation (workflow-run.schema.json).PSEO_WORKSPACE_ROOTenv unset and noworkspace_rootarg passed.local/nap.jsonpresent but unparseable → STOP (fail-loudjson.JSONDecodeError; v1.1 GAP-A2). Never audit against half a canonical — fix or delete the file, then re-run. (A MISSING file is NOT a DURUR: the audit runs and emits the MEDIUM "canonical NAP file missing" row instead.)
Hard Constraint Audit (memory-derived)
This skill MUST honor three feedback memory constraints:
feedback_indexing_api_consent— NO autonomous GBP API submit. This skill reads only (DFS business_listings_search is a query endpoint, not a write endpoint). The recommended_action column tells the operator what to change; the operator does the GBP dashboard work manually.feedback_hard_constraints—.mcp.jsonbyte-byte unchanged (the MCP tool reference here lives in SKILL.md, not in the MCP config file; F-16 invariant intact).feedback_ai_disclosure_ban— does not apply directly (gbp-audit is a discovery skill, not a content production skill; no HTML output).
Changelog
- v1.1 (2026-06-10, GAP-A2 / unified batch GAP-A-B2): Step-4
identity source FIXED — the v1.0 contract read
brand_identity.business_name/brand_identity.primary_locationfrom project.config, keys that have never existed inproject-config.schema.json#brand_identity(visual-identity block,additionalProperties: false) — a phantom contract that could not validate against any live config. v1.1 reads the canonicallocal/nap.json(fallbackconfig["display_name"]). NAP-mismatch branch IMPLEMENTED (declared in the severity matrix since v1.7 Phase 5 but never coded): per-field HIGH rows viascripts.discovery.nap_consistency.compare_nap; canonical file missing → one MEDIUM row. New optional inputnap_source; new DURUR #9 (corruptnap.jsonfail-loud). Rules:rules/local-seo-discipline.md(R-144/R-145/R-146). - v1.0 (v1.7 Phase 5): Initial release — G-AI-02 finding closure (8-category GBP gap audit, profile gate, budget pre-flight, read-only consent posture).
Cross-references
- Schemas:
schemas/master-excel.schema.json#gbp_audit(7 cols; severityEnum + statusEnum reuse),schemas/local-nap.schema.json(canonical NAP doc atprojects/{slug}/local/nap.json, R-144),schemas/events.schema.json(source.kind=dataforseo_mcp,target_excel_sheet=gbp_audit,cost.creditsper ADR-016),schemas/skill-frontmatter.schema.json(this frontmatter). - Rules:
rules/local-seo-discipline.md(R-144 NAP single source of truth · R-145 review-acquisition white-hat · R-146 location-page anti-doorway). - Cross-modules (IMPORT-only, not modified):
scripts/state/workflow_runner.py,scripts/excel/transaction.py,scripts/state/events_writer.py,scripts/budget/check_budget.py,scripts/reporting/render_template.py. - Transform:
scripts/discovery/gbp_audit_transform.py(runpublic entrypoint,preflight_budget,_analyze_gapsgap analysis + severity matrix,GbpAuditError/BudgetExceededErrorexception hierarchy). - Tests:
tests/skills/test_gbp_audit.py(≥4 cases incl. profile-gate skip + gap rows + severity assignment + budget pre-flight). - Template:
templates/reports/gbp-audit.template.md(Phase 11 W deliverable — runtime render integration; skill can ship without the template and emit a minimal report inline).
Discipline checklist
- Profile-gated —
"local-service"requirement enforced at Step 1 BEFORE any paid call. e-commerce / b2b-saas / portfolio pure plays skip cleanly withstatus=skipped. - Read-only audit — NO GBP API submit per
feedback_indexing_api_consent. The skill only queries listings and emits a gap report; operator does the GBP dashboard work. - Budget pre-flight integration —
scripts.budget.check_budgetinvoked at Step 2 BEFORE any paid call (ADR-016 SSoT). - Append-only — all events via
events_writer; per-DFS-call provenance event carriescost.credits(ADR-016). - Schema-first — frontmatter validates against
schemas/skill-frontmatter.schema.jsonDraft 7; gbp_audit rows validate againstmaster-excel.schema.json7 cols (severityEnum + statusEnum reuse). - Plugin-agnostik — no slug literals;
project_slugflows through every path. - severityEnum strict — every emitted row's
severityis in {CRITICAL, HIGH, MEDIUM, LOW}; defensive_validate_rowruns twice (transform + transaction.append). - statusEnum strict — emitted rows use
status="TODO"(seed); operator transitions via done_protocol (rules cross-link). - F5:
outputs.*values are STRING-TYPED (artifact paths or stringified counts), never raw ints.