Compare commits
81 Commits
bb1057ea36
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e2399d600e | |||
| 3d3891aa52 | |||
| a778fe8b65 | |||
| 52283c2254 | |||
| 3e2632a990 | |||
| 96adbf2bd7 | |||
| 708684bb74 | |||
| 5341f3f965 | |||
| 7d03068875 | |||
| 8031ef8c64 | |||
| 9433cfbe03 | |||
| 9799515665 | |||
| 0e56b49ac6 | |||
| 7f3e3a1d31 | |||
| 0ef2fea34a | |||
| 782ecfafe6 | |||
| a21d6783a6 | |||
| 81b5641832 | |||
| b7eda9fc7e | |||
| e2465fe159 | |||
| aa41ac5f6f | |||
| 8c59ce5150 | |||
| 3c290680f4 | |||
| 3b0427985b | |||
| 871a142c55 | |||
| 8fb1423897 | |||
| 8bb1d7b791 | |||
| 73204c4810 | |||
| 394d5dae3f | |||
| c951f79a18 | |||
| 1b6b204303 | |||
| 39677ed251 | |||
| 82a2ee55cf | |||
| 3ab77ac191 | |||
| dfdc30d41d | |||
| beee33c3d7 | |||
| 18fe29106b | |||
| 7520f3d7c8 | |||
| d8f88ec03a | |||
| 8bf3d60ec8 | |||
| 5e05531c28 | |||
| 6c7b4589f1 | |||
| a3d49445f2 | |||
| a6e012b191 | |||
| bb1498bc60 | |||
| 0a387470ba | |||
| 7d92f52794 | |||
| 8a86e4f534 | |||
| 282c9d07bc | |||
| 40a5f38000 | |||
| ac51e57dee | |||
| 39e04c4b88 | |||
| 3478ba3101 | |||
| e5c7abfb55 | |||
| 67d492390b | |||
| 9fef3fcf4e | |||
| 705f0ef2b3 | |||
| c225422aa9 | |||
| b242029b6b | |||
| 6b068959fc | |||
| f8c9a34463 | |||
| 3ea97eae2e | |||
| c659b3c42e | |||
| 82d8ea238a | |||
| 7b7c80d4be | |||
| bf4cc78732 | |||
| 9776548224 | |||
| b5378e7ff9 | |||
| f020be161e | |||
| bb353aa7d1 | |||
| d11b294a18 | |||
| 114ef882e2 | |||
| 29e44ee394 | |||
| 4af93b1aa2 | |||
| 6401ee2ab2 | |||
| 78cb36909c | |||
| 377b19eedd | |||
| 8f52e540a1 | |||
| bc6f96f006 | |||
| 33cab38830 | |||
| 74a584cd82 |
@@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
logs/
|
||||
@@ -38,7 +38,9 @@ It includes:
|
||||
### Service Domains
|
||||
|
||||
1. **Customer Path Integrity** — customers can find, trust, contact, book, visit, or otherwise engage without preventable digital friction.
|
||||
2. **AI Visibility Integrity / AEO** — search engines and AI systems can find, understand, and accurately represent the business.
|
||||
2. **AI Visibility Integrity (AEO + GEO)** — search engines and AI systems can find, understand, and accurately represent the business.
|
||||
- **AEO (Answer Engine Optimization)**: make facts extractable and correctly parseable by answer engines (structure, schema, direct-answer blocks, llms.txt, NAP/entity consistency).
|
||||
- **GEO (Generative Engine Optimization)**: make the business a preferred, accurately represented source inside generative narratives (entity authority, tier-1 citations, cross-web consistency, brand-mention density).
|
||||
3. **Local Competitive Awareness** — local market context via competitor visibility, positioning, review movement, and AI-answer comparison.
|
||||
|
||||
### Evidence Model
|
||||
@@ -64,7 +66,7 @@ Field states:
|
||||
```
|
||||
Intake
|
||||
→ evidence capture (website, GBP, NAP, schema, listings)
|
||||
→ AI representation test (buyer-intent queries across models)
|
||||
→ AI representation test (buyer-intent queries across models — covers both extraction surfaces and generative citation/accuracy)
|
||||
→ Silent Customer Loss Map
|
||||
→ evidence classification (tier + source independence)
|
||||
→ draft canonical business record
|
||||
@@ -93,6 +95,7 @@ Core philosophy and architectural decisions are recorded in `docs/decisions/`. S
|
||||
| Scope narrowed: VeriPath first, GeoLocal deferred | `docs/decisions/2026-08-13-narrowing-scope-veripath-first.md` |
|
||||
| VeriPath owns canonical truth | `docs/decisions/2026-08-12-veripath-system-of-record-boundary.md` |
|
||||
| Execution roadmap v0.1 | `docs/roadmap/execution-roadmap-v0.1.md` |
|
||||
| AEO + GEO ingested into AI Visibility Integrity | `docs/decisions/2026-08-17-aeo-geo-ingestion-into-veripath-model.md` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -20,3 +20,5 @@
|
||||
|------|-------|-------|
|
||||
| 2026-08-05 | 1a | Baseline repo created, cold audit pending |
|
||||
| 2026-08-05 | 1a | Snapshot 1 ingested — R1-R6 applied (no threats fired) |
|
||||
|
||||
- **Call log:** `call-log.md` — every client touch, newest first. Hours gate state lives here.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Phoenix Salon + Spa — Call Log
|
||||
|
||||
Conventions: one entry per client touch, newest first. Every entry has: date, participants, items discussed (status per item), actions (owner + due), notes. Hours items carry the gate state explicitly.
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-17 — Findings call (v1 workflow)
|
||||
|
||||
**Participants:** Tony, Ashley Thornhill (owner)
|
||||
**Duration:** ~14 min
|
||||
**Evidence basis:** 4-surface capture 2026-08-16/17 (`extractions/phoenix_salon__spa_multi_surface_2026-08-17.json`); original audit 2026-08-13
|
||||
|
||||
### Items discussed
|
||||
|
||||
| # | Item | Status after call |
|
||||
|---|---|---|
|
||||
| 1 | Website SSR: initial HTML 13 words (Lovable Vite SPA, no SSR) | **OPEN** — fix assigned to Tony |
|
||||
| 2 | Hours: all surfaces disagree | **OPEN** — gate not passed (see below) |
|
||||
| 3 | Rebrand residue: "La Mae" in 6 locations | **OPEN** — owner to request directory changes |
|
||||
| 4 | GBP gaps: no posts, no Q&A, no price band | **OPEN** — we draft, Ashley posts |
|
||||
|
||||
### HOURS — GATE NOT PASSED (non-negotiable)
|
||||
|
||||
Call claimed hours were settled ("the posted hours on GBP reflect what you've already decided"). **This is not a lock.** No written numbers were captured. Conflicts from the 2026-08-17 capture:
|
||||
|
||||
| Day | GBP | Website (JSON-LD) | Conflict |
|
||||
|---|---|---|---|
|
||||
| Mon–Fri | 9:00 AM – 6:00 PM | 9:00 AM – 7:00 PM | **OPEN — close time** |
|
||||
| Saturday | 9:00 AM – 6:00 PM | 9:00 AM – 5:00 PM | **OPEN — close time** |
|
||||
| Sunday | Closed | 9:00 AM – 5:00 PM (open) | **OPEN — open vs closed** |
|
||||
|
||||
Apple Maps (10:00 AM – 6:00 PM daily incl. Sunday) and Yelp (per 08-13: Mon–Fri 9–7, Sat 9–7, Sun closed) also in conflict; not re-verified 08-17 (Apple is process-gated, Yelp headless-blocked) — re-verify before fixing either.
|
||||
|
||||
Canonical record `hours.regular_hours` was marked `verified/tier_1` from a 2026-08-16 handoff with no owner-written numbers on file → **demoted to OPEN, tier_2, 2026-08-17.**
|
||||
|
||||
**Next action (FIRST gate on next touch):** text Ashley — "Confirming final hours, please send written: Mon __, Tue __, Wed __, Thu __, Fri __, Sat __, Sun __." Written numbers in → record updated → gate passed. **Monitoring handoff is blocked until this closes.**
|
||||
|
||||
### Actions from call
|
||||
|
||||
| # | Action | Owner | Due |
|
||||
|---|---|---|---|
|
||||
| 1 | Hours: written confirmation of all 7 days | Ashley (Tony sends the ask) | next touch, GATE |
|
||||
| 2 | Lovable SSR fix: paste SSR prompt to Lovable AI → Publish → re-crawl, before/after word count | Tony | Phase 2 |
|
||||
| 3 | Rebrand residue (6 locations: CityFeet, BestProsInTown slug, Facebook bio, Yelp "formerly La Mae", Vagaro review, directory listing): list is 4 days old and Yelp is headless-blocked — re-verify each before touching; then Ashley requests name changes on directories she controls | Tony (verify + drafts), Ashley (directory requests) | Phase 2 |
|
||||
| 4 | GBP posts + Q&A + price band: draft content, Ashley posts (GBP is her profile) | Tony (draft), Ashley (post) | Phase 2 |
|
||||
| 5 | Cost log: minutes per fix, started on first fix | Tony | Phase 2 |
|
||||
|
||||
### Notes
|
||||
|
||||
- Call ran v1 (pre-workflow). GEO/AEO education was delivered on call — should have been the one-pager. Not repeating.
|
||||
- No pricing discussed (no economics-v1.md for Phoenix yet; pricing module not in script — rule followed).
|
||||
- Audit timeline for narrative (do not mislabel on future calls): June 2026 (~45/100) → July 23 (82/100, post-Lovable-rebuild) → Aug 6 (68/100) → Aug 13 (VeriPath-era GBP audit) → Aug 16–17 (4-surface + GEO). Scores use different rubrics; never quote cross-rubric numbers to the owner.
|
||||
|
||||
---
|
||||
|
||||
+71
-24
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"record_id": "VP-001-phoenix-salon-spa-v0.1.1",
|
||||
"record_version": "0.1.1",
|
||||
"record_version": "v0.1.2",
|
||||
"record_type": "canonical_business_record",
|
||||
"system": {
|
||||
"authority": "veripath",
|
||||
"created_at": "2026-08-12T23:30:00Z",
|
||||
"updated_at": "2026-08-12T23:30:00Z",
|
||||
"updated_at": "2026-08-16T10:30:00Z",
|
||||
"status": "draft"
|
||||
},
|
||||
"domains": {
|
||||
@@ -138,19 +138,19 @@
|
||||
"Wed": "09:00-18:00",
|
||||
"Thu": "09:00-18:00",
|
||||
"Fri": "09:00-18:00",
|
||||
"Sat": "09:00-15:00",
|
||||
"Sat": "09:00-18:00",
|
||||
"Sun": "closed"
|
||||
},
|
||||
"status": "verified",
|
||||
"evidence_tier": "tier_1",
|
||||
"observed_at": "2026-08-11T00:00:00Z",
|
||||
"verified_at": "2026-08-12T23:30:00Z",
|
||||
"fresh_until": "2026-08-19T23:30:00Z",
|
||||
"source_independence": "unknown",
|
||||
"status": "open",
|
||||
"evidence_tier": "tier_2",
|
||||
"observed_at": "2026-08-16T00:00:00Z",
|
||||
"verified_at": "2026-08-16T10:30:00Z",
|
||||
"fresh_until": "2026-09-16T00:00:00Z",
|
||||
"source_independence": "independent",
|
||||
"sources": [
|
||||
"owner ground truth (Ashley) 2026-08-11"
|
||||
"owner ground truth (Ashley) via Leonard handoff 2026-08-16"
|
||||
],
|
||||
"notes": "OWNER-VERIFIED GROUND TRUTH. All other public surfaces carry INCORRECT hours. Website/schema say Mon-Fri 9-7, Sat-Sun 9-5. Yelp says Mon-Fri 9-7, Sat 9-7. GBP not verified against owner truth. Operating model is by-appointment; no canonical hours previously existed."
|
||||
"notes": "REOPENED 2026-08-17: 2026-08-17 capture shows conflicts GBP Mon-Sat 9-6 / website (JSON-LD) Mon-Fri 9-7 Sat-Sun 9-5 / Apple 10-6 daily incl Sun / Yelp (08-13) Mon-Fri 9-7 Sat 9-7 Sun closed. 2026-08-17 findings call claimed verbal settlement but no written numbers were captured, so the hours GATE (client-call-workflow-v2.md, rule 3) was not passed. Owner-written confirmation of all 7 days required before status returns to verified and before any monitoring handoff."
|
||||
},
|
||||
"by_appointment_only": {
|
||||
"value": true,
|
||||
@@ -193,7 +193,9 @@
|
||||
"verified_at": null,
|
||||
"fresh_until": null,
|
||||
"source_independence": "same_origin",
|
||||
"sources": ["website /services page 2026-08-11"]
|
||||
"sources": [
|
||||
"website /services page 2026-08-11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Full Color",
|
||||
@@ -206,7 +208,9 @@
|
||||
"verified_at": null,
|
||||
"fresh_until": null,
|
||||
"source_independence": "same_origin",
|
||||
"sources": ["website /services page 2026-08-11"]
|
||||
"sources": [
|
||||
"website /services page 2026-08-11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Balayage / Highlights",
|
||||
@@ -219,7 +223,9 @@
|
||||
"verified_at": null,
|
||||
"fresh_until": null,
|
||||
"source_independence": "same_origin",
|
||||
"sources": ["website /services page 2026-08-11"]
|
||||
"sources": [
|
||||
"website /services page 2026-08-11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Extensions",
|
||||
@@ -232,7 +238,9 @@
|
||||
"verified_at": null,
|
||||
"fresh_until": null,
|
||||
"source_independence": "same_origin",
|
||||
"sources": ["website /services page 2026-08-11"]
|
||||
"sources": [
|
||||
"website /services page 2026-08-11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Phoenix Custom Facial",
|
||||
@@ -245,7 +253,9 @@
|
||||
"verified_at": null,
|
||||
"fresh_until": null,
|
||||
"source_independence": "same_origin",
|
||||
"sources": ["website /services page 2026-08-11"]
|
||||
"sources": [
|
||||
"website /services page 2026-08-11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Spray Tan",
|
||||
@@ -258,7 +268,9 @@
|
||||
"verified_at": null,
|
||||
"fresh_until": null,
|
||||
"source_independence": "same_origin",
|
||||
"sources": ["website /services page 2026-08-11"]
|
||||
"sources": [
|
||||
"website /services page 2026-08-11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Waxing Services",
|
||||
@@ -271,7 +283,9 @@
|
||||
"verified_at": null,
|
||||
"fresh_until": null,
|
||||
"source_independence": "same_origin",
|
||||
"sources": ["website /services page 2026-08-11"],
|
||||
"sources": [
|
||||
"website /services page 2026-08-11"
|
||||
],
|
||||
"notes": "Full waxing price menu NOT_OBSERVED; page links to sub-menus not captured."
|
||||
},
|
||||
{
|
||||
@@ -285,7 +299,9 @@
|
||||
"verified_at": null,
|
||||
"fresh_until": null,
|
||||
"source_independence": "same_origin",
|
||||
"sources": ["website /services page 2026-08-11"]
|
||||
"sources": [
|
||||
"website /services page 2026-08-11"
|
||||
]
|
||||
}
|
||||
],
|
||||
"price_range": {
|
||||
@@ -380,7 +396,9 @@
|
||||
"verified_at": null,
|
||||
"fresh_until": null,
|
||||
"source_independence": "same_origin",
|
||||
"sources": ["website schema sameAs"]
|
||||
"sources": [
|
||||
"website schema sameAs"
|
||||
]
|
||||
},
|
||||
"facebook": {
|
||||
"value": "facebook.com/PhoenixCameronPark",
|
||||
@@ -390,7 +408,9 @@
|
||||
"verified_at": null,
|
||||
"fresh_until": null,
|
||||
"source_independence": "same_origin",
|
||||
"sources": ["website schema sameAs"]
|
||||
"sources": [
|
||||
"website schema sameAs"
|
||||
]
|
||||
},
|
||||
"tiktok": {
|
||||
"value": "present in website footer",
|
||||
@@ -400,7 +420,9 @@
|
||||
"verified_at": null,
|
||||
"fresh_until": null,
|
||||
"source_independence": "same_origin",
|
||||
"sources": ["website footer"]
|
||||
"sources": [
|
||||
"website footer"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -408,7 +430,7 @@
|
||||
{
|
||||
"field": "hours.regular_hours",
|
||||
"description": "Owner ground truth conflicts with all public surfaces",
|
||||
"owner_truth": "Mon-Fri 9-6, Sat 9-3, Sun closed",
|
||||
"owner_truth": "Mon–Sat 9–6, Sun closed",
|
||||
"website": "Mon-Fri 9-7, Sat-Sun 9-5",
|
||||
"yelp": "Mon-Fri 9-7, Sat 9-7, Sun closed",
|
||||
"gbp": "NOT_VERIFIED against owner truth",
|
||||
@@ -435,6 +457,19 @@
|
||||
"action_risk": "low",
|
||||
"status": "unresolved",
|
||||
"discovered_at": "2026-08-05T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "hours-surfaces-2026-08-17",
|
||||
"field": "hours.regular_hours",
|
||||
"status": "open",
|
||||
"evidence": {
|
||||
"gbp": "Mon-Sat 09:00-18:00, Sun closed",
|
||||
"website": "Mon-Fri 09:00-19:00, Sat-Sun 09:00-17:00 (JSON-LD, 2026-08-17)",
|
||||
"apple_maps": "daily 10:00-18:00 incl Sunday (2026-08-17)",
|
||||
"yelp": "Mon-Fri 09:00-19:00, Sat 09:00-19:00, Sun closed (2026-08-13, not re-verified)"
|
||||
},
|
||||
"owner_truth": null,
|
||||
"resolution_required": "Owner-written confirmation of all 7 days (hours GATE). No written numbers = stays open."
|
||||
}
|
||||
],
|
||||
"approvals": [],
|
||||
@@ -443,6 +478,18 @@
|
||||
"timestamp": "2026-08-12T23:30:00Z",
|
||||
"action": "created",
|
||||
"detail": "Initial draft canonical business record compiled from existing evidence: GBP snapshot 2026-08-05, website capture 2026-08-11, owner ground truth 2026-08-11, live web search 2026-08-12"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-08-16T10:30:00Z",
|
||||
"action": "updated",
|
||||
"field": "hours.regular_hours",
|
||||
"detail": "Corrected Saturday hours from 09:00-15:00 to 09:00-18:00 per owner ground truth. Source: Leonard handoff 2026-08-16. This is now the authoritative baseline for all surface diffs."
|
||||
},
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"field": "hours.regular_hours",
|
||||
"detail": "Reopened hours: demoted verified/tier_1 to open/tier_2. 2026-08-17 capture conflicts (GBP Mon-Sat 9-6 vs website Mon-Fri 9-7 Sat-Sun 9-5) were not locked on the 2026-08-17 call despite verbal 'settled' claim; no written numbers on file. Hours GATE per client-call-workflow-v2.md. Prior baseline Mon-Sat 9-6 Sun closed set 2026-08-16 from Leonard handoff — superseded until owner confirms in writing.",
|
||||
"source": "2026-08-17 findings call + multi_surface_2026-08-17 capture"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,8 @@ Establish ground truth of the client's digital presence. No analysis or diagnosi
|
||||
- **Notes / Gaps:** Booking button observed on GBP ("Book online"); destination not yet confirmed
|
||||
- **Snapshot Location:** `client-data-pss/raw/booking/`
|
||||
|
||||
**Hours status:** OPEN — all four surfaces disagree (2026-08-17 capture: GBP Mon–Sat 9–6 · website Mon–Fri 9–7, Sat–Sun 9–5 · Apple 10–6 daily incl Sun · Yelp 08-13 Mon–Fri 9–7, Sat 9–7, Sun closed). Owner-written confirmation of all 7 days required before status changes (hours GATE, `client-call-workflow-v2.md`). Canonical record `hours.regular_hours` demoted to open/tier_2 on 2026-08-17.
|
||||
|
||||
## Summary of Gaps / Blockers
|
||||
- Services, posts, Q&A not yet observed on GBP
|
||||
- Claim status unknown — needs owner access or deeper verification
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Phoenix Salon + Spa — Owner Onboarding Pre-Work Sheet
|
||||
**Call:** 15 min · screen share on (Apple step needs it) · owner's phone within reach (Apple may text/call)
|
||||
**Source of truth:** `canonical-business-record-v0.1.1.json` @ `3b04279`
|
||||
**SOP:** `docs/operations/owner-onboarding-sop.md` (v1.1)
|
||||
|
||||
---
|
||||
|
||||
## 1 · Canonical Read-Back (read aloud, one at a time)
|
||||
Ask each: *"Is this right, or do I have it wrong?"* Log the spoken answer.
|
||||
|
||||
- **Name:** Phoenix Salon + Spa (formerly La Mae Salon & Spa)
|
||||
- **Address:** 3460 Robin Ln, Suite 4, Cameron Park, CA 95682
|
||||
- **Phone:** (530) 350-9197
|
||||
- **Website:** phoenixcameronpark.com
|
||||
- **Hours —** read slowly:
|
||||
- Monday: 9:00 AM to 6:00 PM
|
||||
- Tuesday: 9:00 AM to 6:00 PM
|
||||
- Wednesday: 9:00 AM to 6:00 PM
|
||||
- Thursday: 9:00 AM to 6:00 PM
|
||||
- Friday: 9:00 AM to 6:00 PM
|
||||
- Saturday: 9:00 AM to 6:00 PM
|
||||
- Sunday: **Closed**
|
||||
- **Booking:** By appointment only (Vagaro)
|
||||
- **Category:** Beauty salon / spa
|
||||
|
||||
> If she corrects anything → that value wins. Update the canonical record BEFORE the Apple step.
|
||||
|
||||
---
|
||||
|
||||
## 2 · Current Problems (plain language — say, don't show)
|
||||
- **Apple Maps is wrong:** it shows the shop open 10 AM–6 PM **every day, including Sunday**. It's not even the right open time and it claims we're open Sunday. We'll fix that in about three minutes on screen share.
|
||||
- **The website is wrong:** the machine-readable hours embedded in the site say we close at 7 PM (Mon–Fri) / 5 PM (Sat–Sun) and that we're **open Sunday**. That's what feeds search engines and AI assistants. We'll make a one-time fix to that — we don't control the site, so it's a one-time correction, not ongoing.
|
||||
- *(Google and Bing are currently correct — no fix needed there.)*
|
||||
|
||||
---
|
||||
|
||||
## 3 · Access Steps
|
||||
|
||||
### 3a — Google Business Profile manager invite (~2 min, she does it)
|
||||
She's signed into her own Google account at `business.google.com`:
|
||||
1. Select **Phoenix Salon + Spa**
|
||||
2. Pencil icon → **Edit business information** → **People and permissions**
|
||||
3. **Add people** → paste this email: **`{VERIPATH_GBP_EMAIL}`** ← ⚠️ FILL BEFORE CALL
|
||||
4. Role: **Edit business info** (explicitly *not* "Manage")
|
||||
5. Send
|
||||
|
||||
Say: *"You keep full ownership. I get the smallest key that opens the door I need — edit info only, and you can pull it anytime."*
|
||||
Revoke: same page → her account → **Remove**. One click.
|
||||
|
||||
### 3b — Apple Business Connect (screen share, ~3 min, she does it)
|
||||
We can't do this for her — Apple gates it on identity.
|
||||
1. Screen share. She opens **abc.apple.com**, signs in to her Apple ID (create one if needed, ~1 min)
|
||||
2. **Claim a business** → search **"Phoenix Salon + Spa"** → select the listing
|
||||
3. **Verify ownership** — prefer **video** (fastest); phone-code is fallback; postcard = slow, log as pending
|
||||
4. Once claimed, set hours from the read-back values: Mon–Sat 9–6, **Sun closed** — confirm each day on screen
|
||||
5. Say: *"That was the one thing only you could do. It's done. If Apple's review lags a day or two, that's Apple — and we'll watch it."*
|
||||
|
||||
Fallback if claim fails: **Suggest an edit** from Apple Maps on her listing → log pending, loop stays read-only on Apple until it lands.
|
||||
|
||||
### 3c — Website (no access grant)
|
||||
One-time JSON-LD hours fix under existing Lovable access. Say: *"We've fixed the hours markup. That was one-time — we don't control the site, and we're not pretending we do."*
|
||||
|
||||
---
|
||||
|
||||
## 4 · Post-Call Checklist
|
||||
- [ ] Update canonical record if owner corrected anything (bump version, `verified`/`tier_1`, commit)
|
||||
- [ ] One-time Lovable JSON-LD hours fix (log it)
|
||||
- [ ] Re-run full audit pipeline + strict gate (hours finding should drop to Apple-only)
|
||||
- [ ] Generate + send trust receipt (include any `gaps`)
|
||||
- [ ] Start monitoring clock — **daily × 30 days**, then weekly (DR-002)
|
||||
|
||||
---
|
||||
|
||||
## 5 · Timing Log (fill during call)
|
||||
| Phase | Target | Actual |
|
||||
|---|---|---|
|
||||
| P1 Frame | 2 min | ____ |
|
||||
| P2 Lock ground truth | 5 min | ____ |
|
||||
| P3a GBP invite | 2 min | ____ |
|
||||
| P3b Apple claim+hours | 3 min | ____ |
|
||||
| P4 Operating agreement + close | 3 min | ____ |
|
||||
| **Total** | **15 min** | **____** |
|
||||
|
||||
Notes (what was awkward, gaps, owner pushback):
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Decision: Audit pipeline promoted to v1.1
|
||||
|
||||
**Date:** 2026-08-15
|
||||
**Status:** Approved
|
||||
**Supersedes:** Locked v1.0 process (2026-08-15) — same date, same day, tighter mechanics
|
||||
|
||||
## Change
|
||||
|
||||
`beta-audit-process.md` moves from **Locked v1.0** to **Locked v1.1**.
|
||||
The locked process said *what* must be true (report, gate, artifact set).
|
||||
v1.1 wires the *how* into the pipeline so the guarantees hold without
|
||||
human ritual.
|
||||
|
||||
## What changed in the toolchain
|
||||
|
||||
`implementation/auditing/` gains two scripts and the pipeline grows from
|
||||
2 steps to 4:
|
||||
|
||||
```text
|
||||
multi_scraper → audit_engine → report_generate → report_gate
|
||||
```
|
||||
|
||||
1. **`report_generate.py`** — builds `REPORT-final.md` + `VALIDATION.md`
|
||||
from the run's `findings.json` + raw capture. Per-surface status in the
|
||||
Surfaces Reviewed table is derived from `summary.surfaces_ok`
|
||||
(✓/✗/⚠ with reason); a Data Limitations section states the known
|
||||
ceilings (no review text, no authenticated hours, point-in-time).
|
||||
2. **`report_gate.py`** — the §6 pre-delivery gate, scripted. Four checks:
|
||||
count match, evidence present (absence-type findings re-derived from
|
||||
raw capture instead of requiring quoted evidence), identity match, and
|
||||
material support (every material finding re-derived against the raw
|
||||
capture). Non-zero exit on failure; result stamped into `VALIDATION.md`.
|
||||
3. **`audit_diff.py`** — temporal delta between two raw captures →
|
||||
`DELTA.md` in the after-run folder. The proof-of-fix artifact: after the
|
||||
client applies recommendations, re-run and the delta is the invoice
|
||||
evidence.
|
||||
4. **`audit_pipeline.sh`** — steps 3–4 wired in; gate FAIL blocks delivery
|
||||
(`exit 1`). Capture copy is collision-safe: a re-run never overwrites
|
||||
the prior capture (timestamped sibling file), which is what makes the
|
||||
temporal baseline in `audit_diff.py` possible.
|
||||
|
||||
## Why
|
||||
|
||||
- The v1.0 gate was manual; a 9-run batch showed the manual gate drifts
|
||||
(reports were regenerated by a tmp script outside the repo).
|
||||
- Re-runs overwrote prior captures, so before/after proof was impossible.
|
||||
- The report builder lived in `/tmp`, not the repo, so "report is a
|
||||
pipeline output" was a convention, not a mechanism.
|
||||
|
||||
## Verification (2026-08-15)
|
||||
|
||||
- All 9 runs of the 2026-08-15 batch regenerated through
|
||||
`report_generate.py`; `report_gate.py` PASS 9/9.
|
||||
- Live end-to-end proof: Gilmore Heating, Air and Plumbing re-run through
|
||||
the full 4-step pipeline. Baseline capture preserved as a timestamped
|
||||
sibling; report regenerated; gate PASS; `audit_diff.py` produced
|
||||
`DELTA.md` (0 field changes — same day, as expected).
|
||||
|
||||
## No change to
|
||||
|
||||
- Scope (four surfaces), intake, reviewer disposition loop, blocked-surface
|
||||
treatment (§4), delivery. v1.1 changes mechanics, not meaning.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Decision: Ingest AEO vs GEO Distinction into VeriPath Model
|
||||
|
||||
**Date:** 2026-08-17
|
||||
**Status:** Accepted
|
||||
**Context:** Critical external knowledge from Smart Money Media (https://smartmoneymedia.org/blog/aeo-vs-geo-answer-engine-optimization-generative-engine and the definitive guide at /guides/aeo-vs-geo) must be incorporated so VeriPath's AI Visibility domain is precise and future-proof.
|
||||
|
||||
## Summary of Source Knowledge
|
||||
|
||||
Industry definitions of AEO and GEO are still converging, but two complementary framings are useful:
|
||||
|
||||
### Framing A (Smart Money Media definitive guide, Aug 2026)
|
||||
- **AEO (Answer Engine Optimization)**: Focused on conversational AI engines (ChatGPT, Perplexity, Claude, Gemini, Grok, Copilot chat). Win condition = cited by name inside the generated answer.
|
||||
- **GEO (Generative Engine Optimization)**: Broader umbrella that includes AEO + Google AI Overviews, Bing generative answers, and other generative search surfaces. AEO is a sub-discipline of GEO.
|
||||
- Shared ~80% infrastructure: entity graph, schema, llms.txt, direct-answer content blocks, tier-1 earned media, AI crawler access, freshness.
|
||||
- Classic SEO (top-10 organic) is high-dependency for Google AI Overviews (GEO), moderate for pure chat AEO.
|
||||
|
||||
### Framing B (earlier blog + other sources)
|
||||
- **AEO**: Extraction-oriented. Structure pages so machines can lift self-contained passages (featured snippets, AI Overviews, voice, PAA).
|
||||
- **GEO**: Reputation / citation-oriented. Build third-party authority and entity consistency so generative models choose to name and correctly synthesize the brand.
|
||||
|
||||
Both framings agree on core tactics VeriPath already prioritizes: clean entity identity, structured facts, human-verified canonical record, tiered evidence, and measurement of AI representation.
|
||||
|
||||
## Incorporation into VeriPath
|
||||
|
||||
1. **Service Domain 2** is expanded from "AI Visibility Integrity / AEO" to **AI Visibility Integrity (AEO + GEO)**.
|
||||
2. Definitions are locked as follows for VeriPath operating language:
|
||||
- **AEO**: The practice of making the business's facts extractable and correctly parseable by answer engines (structure, schema, direct-answer blocks, llms.txt, NAP/entity consistency).
|
||||
- **GEO**: The practice of making the business a preferred, accurately represented source inside generative narratives (entity authority, tier-1 citations, cross-web consistency, brand-mention density in training/retrieval corpora).
|
||||
3. The canonical business record remains the single source of truth that powers both AEO (structured extractable facts) and GEO (trusted entity signals).
|
||||
4. AI representation tests in the audit workflow must cover both:
|
||||
- Extraction surfaces (AI Overviews, featured snippets where applicable).
|
||||
- Generative citation and accuracy across major chat engines.
|
||||
5. No change to evidence tiers or human-in-the-loop rule. Both AEO and GEO findings remain subject to Tier 1/2 classification and owner approval.
|
||||
|
||||
## Rationale
|
||||
Local businesses lose customers when either (a) the AI cannot extract a clean answer or (b) the AI extracts/synthesizes the wrong competitor. VeriPath's strength is the verified canonical record that closes both failure modes. Explicit AEO/GEO language prevents under-specification when talking to prospects, agencies, or building future measurement.
|
||||
|
||||
## Follow-on Actions
|
||||
- Update README.md Service Domains section.
|
||||
- Update any homepage / methodology copy that only says "AEO".
|
||||
- Ensure cold-audit AI representation prompts explicitly test both classes of surfaces.
|
||||
- Record this decision in the project memory and operating truth.
|
||||
|
||||
**Owner:** Tony / VeriPath
|
||||
**Verified against source:** Smart Money Media AEO vs GEO materials (Mar–Aug 2026).
|
||||
@@ -0,0 +1,50 @@
|
||||
# Decision / Synthesis: Leonard AEO vs GEO Refinement + Local Adaptation (2026-08-17)
|
||||
|
||||
**Date:** 2026-08-17
|
||||
**Status:** Accepted — extends 2026-08-17 primary ingestion
|
||||
**Source:** Leonard synthesis of Smart Money Media AEO/GEO material (fact store id 115) + Conductor 2026 benchmark cross-check
|
||||
|
||||
## Core Split (locked)
|
||||
|
||||
- **AEO = extraction.** Schema markup, question-formatted H2s, 40–60 word direct answers, `llms.txt`, clean HTML tables/structure. Makes pages parseable by answer engines. Typical impact window: 2–6 weeks.
|
||||
- **GEO = reputation / citation.** Entity clarity (Wikidata / Knowledge Panel where eligible), citation density, NAP consistency across surfaces, earned media (adapted for local). Makes the brand the preferred source that generative models choose to name and synthesize correctly. Typical impact window: 3–6 months.
|
||||
- LLMs are the engine; AEO and GEO are the complementary strategies. “AI SEO” remains the vague umbrella; GEO is the more precise playbook once extraction is solved.
|
||||
|
||||
## Mid-2026 Convergence (load-bearing)
|
||||
|
||||
Engine shifts have killed pure “pick one” framing:
|
||||
|
||||
1. Google AI Overviews now pull third-party generative citations at materially higher rates than mid-2025 — AEO surfaces increasingly reward GEO signals (authority + consistency).
|
||||
2. ChatGPT’s retrieval layer is hybrid (Bing index + OAI-SearchBot). Classic Bing rankings / Places presence are now inputs to ChatGPT answers, not only a Google problem.
|
||||
3. Perplexity (and similar) weight structured-answer content in source selection — GEO surfaces reward AEO signals (clean extractable blocks).
|
||||
|
||||
**Net result for VeriPath:** One integrated program (same schema stack, entity consistency work, measurement loop). Differentiation happens only at the reporting layer (extraction failures vs citation/surface-drift failures).
|
||||
|
||||
## Local Adaptation (critical correction)
|
||||
|
||||
Smart Money Media / B2B GEO advice centers on tier-1 editorial (Forbes, Bloomberg, TechCrunch). For local businesses the high-value trust nodes are different:
|
||||
|
||||
- Google Business Profile (and consistency with site schema)
|
||||
- Yelp / review aggregators
|
||||
- Local press, directories, chamber / association citations
|
||||
- Cross-surface NAP + entity consistency (the “surface drift” signal)
|
||||
|
||||
**Surface drift** (conflicting phone numbers, addresses, hours, or category signals across GBP / site / listings / schema) is now the primary GEO-side failure mode that breaks AI recommendations. The 2× AIO citation rate increase makes this drift more consequential: inconsistent entities get dropped or replaced by competitors that look cleaner to the model.
|
||||
|
||||
## Audit Scope Addition
|
||||
|
||||
- Explicitly test Bing Places / Bing organic state as a ChatGPT retrieval input.
|
||||
- Flag surface drift as a GEO signal with clear before/after measurement potential.
|
||||
- Keep Conductor-style aggregate numbers (ChatGPT ~87% of AI referral traffic, ~1% MoM growth, industry variance) as directional context only — not client-facing claims without independent verification.
|
||||
|
||||
## Positioning Implication
|
||||
|
||||
VeriPath’s Cold Audit already diagnoses both sides:
|
||||
- Extraction failure → AEO work (structure, schema, direct answers, `llms.txt`)
|
||||
- Citation / surface-drift failure → GEO work (entity authority via directories + local citations + consistency)
|
||||
|
||||
This synthesis validates the existing SEO + AEO + GEO framing and supplies the mechanism language for the Phoenix live call and future methodology copy.
|
||||
|
||||
**Follow-on:** Update any positioning / SOP language that still treats AEO and GEO as fully separable workstreams. Prefer “integrated integrity program, reported as two failure modes.”
|
||||
|
||||
**Owner:** Tony / VeriPath (with Leonard input)
|
||||
@@ -1,4 +1,4 @@
|
||||
# DOP Homepage Blueprint (Draft v0.1 — collaborative, not yet committed)
|
||||
# VeriPath Homepage Blueprint (Draft v0.1 — collaborative, not yet committed)
|
||||
**Authors**: Qwen (structure) + Leonard (evidence grounding) · **Date**: 2026-08-10
|
||||
**Brand**: red-accent `#b8402c`, rigorous-engineering tone, no marketing fluff
|
||||
**Evidence rule**: every claim maps to Tier 1 (Verified) or Tier 2 (Indicative); human-approved.
|
||||
@@ -10,12 +10,12 @@
|
||||
|
||||
Split visual:
|
||||
- **Left — Industry Standard**: generic "AI citation tool" dashboard, green checkmarks, red overlay showing the customer path leaking ("AI recommended a competitor because their schema was structured better").
|
||||
- **Right — DOP Standard**: a Tier 1 Verified finding with human analyst approval stamp.
|
||||
- **Right — VeriPath Standard**: a Tier 1 Verified finding with human analyst approval stamp.
|
||||
|
||||
**Interactive element (slider/tab)** — anchored on the **Gilmore HVAC** live audit (`research/cold-audits/cold-audit-hvac-gilmore.md`):
|
||||
- Prompt shown: *"Best HVAC company in Cameron Park, CA"* (Perplexity/ChatGPT).
|
||||
- Leak shown: 3 conflicting phone numbers (page `(916) 884-6354` / schema `+1-530-290-5551` / snippet `(530) 344-4515`) + schema geo `Antelope, CA 95843` vs Cameron Park target.
|
||||
- Caption: "The AI doesn't know which number is real. Neither do the customers it sends elsewhere."
|
||||
- Caption: "The AI doesn't know which number is real. Neither do the customers it sends elsewhere."
|
||||
(This is the Screenshot of Shame moment.)
|
||||
|
||||
## Section B — "Three Domains of Integrity"
|
||||
@@ -30,9 +30,9 @@ Split visual:
|
||||
- **Step 4 — Retainer**: Ongoing human-supervised, agent-assisted integrity + monthly measurement loop. *"We do not accept retainers without a successful PoC."*
|
||||
|
||||
## Section D — "Objection Handling" FAQ
|
||||
- **Q: Why not just use an automated AI citation builder?**
|
||||
A: Automated tools generate Tier 2 (indicative) data; LLMs hallucinate or ignore unverified input. DOP relies on Tier 1 (verified) evidence, human-approved, so the AI actually trusts the entity.
|
||||
- **Q: How is this different from Bizl?**
|
||||
- **Q: Why not just use an automated AI citation builder?**
|
||||
A: Automated tools generate Tier 2 (indicative) data; LLMs hallucinate or ignore unverified input. VeriPath relies on Tier 1 (verified) evidence, human-approved, so the AI actually trusts the entity.
|
||||
- **Q: How is this different from Bizl?**
|
||||
A: Bizl shows Tier-3 self-reported case studies. Our 5 live audits are observed, falsifiable evidence — we show our work.
|
||||
|
||||
## Trust Anchor (site-wide footer/header)
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# VeriPath Beta Audit Process
|
||||
|
||||
**Status:** Locked v1.2 — 2026-08-17, for beta use (v1.2: `docs/operations/decisions/DR-003-website-stack-fingerprinting.md`)
|
||||
**Supersedes:** 2026-08-14 draft lock
|
||||
|
||||
## Purpose
|
||||
|
||||
Professional, repeatable audit for beta clients using the automated primary-surface pipeline.
|
||||
|
||||
## Scope
|
||||
|
||||
The beta audit covers four primary surfaces:
|
||||
|
||||
- Google Business Profile
|
||||
- Apple Maps
|
||||
- Bing Places
|
||||
- Current website
|
||||
|
||||
The client-facing report includes material findings only, covering:
|
||||
|
||||
- NAP consistency
|
||||
- Hours consistency, with Google Business Profile as the source of truth
|
||||
- Review velocity
|
||||
- Website reachability, schema conflicts, build stack, and SSR (rendering) signal
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Intake
|
||||
|
||||
Collect:
|
||||
|
||||
- Business name
|
||||
- City
|
||||
- Website
|
||||
- Known previous names
|
||||
|
||||
### 2. Run the pipeline
|
||||
|
||||
Run:
|
||||
|
||||
```text
|
||||
multi_scraper → audit_engine → report_generate → report_gate
|
||||
```
|
||||
|
||||
The pipeline writes the full required artifact set itself. `report_gate`
|
||||
fails the run (non-zero exit) if the §6 gate fails — a failing run does
|
||||
not deliver.
|
||||
|
||||
Website capture also fingerprints the build stack and records the SSR signal
|
||||
(`website.stack.primary`, `website.stack.ssr_signal` in the raw JSON). When
|
||||
`primary` is not `unknown`, the client report states how the website was
|
||||
built. When `ssr_signal` is `weak` (fewer than 50 words in the raw HTML), the
|
||||
report flags that non-JavaScript AI crawlers cannot read the page body, and
|
||||
any capture-based finding about website content must be qualified accordingly.
|
||||
|
||||
### 3. Human validation (reviewer decision loop)
|
||||
|
||||
The reviewer must resolve every engine finding to exactly one disposition.
|
||||
Unresolved findings block delivery.
|
||||
|
||||
| Disposition | Effect |
|
||||
|-------------|--------|
|
||||
| **Approve** | Finding stands as generated |
|
||||
| **Reject** | Finding defaults to **Tier 2** (corroborated, not verified) unless the reviewer (a) discards it entirely, or (b) requests more evidence |
|
||||
| **Request more evidence** | Finding marked `pending`; re-verified on the next run; may not ship as Tier 1 until evidence lands |
|
||||
|
||||
Reviewer decisions are recorded in the run's `VALIDATION.md` (one line per finding: finding id, disposition, rationale).
|
||||
|
||||
### 4. Blocked surfaces
|
||||
|
||||
A surface that fails capture (blocked, 403, no data returned) must never be
|
||||
reported as a bare "✗" with no downstream treatment.
|
||||
|
||||
- A blocked or missing **customer-facing** surface (website, booking page) is a
|
||||
Customer Path Leakage signal: it ships as a finding, or the reviewer records
|
||||
a documented limitation in `VALIDATION.md` with the reason it is not material.
|
||||
- Non-customer-facing surface failures (e.g. Bing empty) are noted in the
|
||||
Surfaces Reviewed table with the failure reason.
|
||||
|
||||
### 5. Client-facing report
|
||||
|
||||
Prepare a report containing material findings only. The report uses the approved
|
||||
VeriPath client-facing structure and voice. It does not include coaching
|
||||
language or marketing urgency.
|
||||
|
||||
### 6. Pre-delivery consistency gate
|
||||
|
||||
Executed by `implementation/auditing/report_gate.py` as pipeline step 4;
|
||||
result stamped into `VALIDATION.md`. All four checks pass before the
|
||||
report is sent. Any failure blocks delivery:
|
||||
|
||||
1. **Count match:** the executive summary's stated number of findings equals the
|
||||
number of findings in the Findings section.
|
||||
2. **Evidence present:** every finding's cited evidence exists in the run's
|
||||
archived artifacts (raw capture or `findings.json`). Absence-type findings
|
||||
(no website, no description, UTM parameters, unreachable surface) are
|
||||
re-derived from the raw capture instead of requiring quoted evidence.
|
||||
3. **Identity match:** report client name, location, and audit date match the
|
||||
intake record.
|
||||
4. **Material support:** each material finding is re-derived against the raw
|
||||
capture (phone cores across surfaces, hours day counts, rating, website
|
||||
presence, description/OG fields).
|
||||
|
||||
### 7. Delivery
|
||||
|
||||
Deliver the validated client-facing report to the client using the locked
|
||||
delivery template.
|
||||
|
||||
## Required artifact set
|
||||
|
||||
Every run is archived under `docs/validation/<YYYY-MM-DD>-<slug>/` with:
|
||||
|
||||
| Artifact | Source |
|
||||
|----------|--------|
|
||||
| `*_multi_surface_YYYY-MM-DD.json` | raw pipeline capture |
|
||||
| `findings.json` | engine output |
|
||||
| `findings.md` | engine markdown |
|
||||
| `REPORT-final.md` | client-facing report (post-review) |
|
||||
| `VALIDATION.md` | run notes, surface results, reviewer decisions |
|
||||
| `DELTA.md` | before/after field comparison (re-runs only) |
|
||||
|
||||
Re-runs never overwrite the prior raw capture: the pipeline writes a
|
||||
timestamped sibling (`*__YYYYMMDD-HHMMSS.json`). The prior capture is the
|
||||
temporal baseline; `audit_diff.py` compares the two into `DELTA.md`.
|
||||
|
||||
Runs completed before 2026-08-15 are grandfathered as-is. Every run on or
|
||||
after this date must complete the set before delivery.
|
||||
|
||||
## Out of Scope for Default Beta
|
||||
|
||||
The following are not included in the default beta audit:
|
||||
|
||||
- Exhaustive secondary-directory research
|
||||
- Full AI chatbot visibility scoring
|
||||
- Competitive benchmarking
|
||||
- Social content audits
|
||||
|
||||
## Conditional Legacy Check
|
||||
|
||||
Run a quick old-name and old-website check only when a name or location change
|
||||
in the last 3–5 years is known or indicated during intake. This check is not
|
||||
part of every default beta run.
|
||||
|
||||
## Pipeline Reference
|
||||
|
||||
The working pipeline is located at:
|
||||
|
||||
```text
|
||||
implementation/auditing/
|
||||
```
|
||||
|
||||
The standard execution path is:
|
||||
|
||||
```text
|
||||
multi_scraper → audit_engine → report_generate → report_gate
|
||||
audit_diff (re-runs: baseline capture vs. new capture)
|
||||
```
|
||||
|
||||
## Deviations
|
||||
|
||||
Any deviation from this process requires a dated decision record in
|
||||
`docs/decisions/`. Edits to this document follow the same rule: the change is a
|
||||
dated decision, not a silent edit.
|
||||
|
||||
## Deeper Engagements
|
||||
|
||||
The full Layer 1/2 system remains available for deeper engagements. Beta audits
|
||||
use the streamlined path above.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Client Call Workflow v2
|
||||
|
||||
**Status:** Active
|
||||
**Effective:** 2026-08-17
|
||||
**Replaces:** ad-hoc findings calls (v1, first run 2026-08-17 with Phoenix Salon + Spa — see lessons learned, `docs/clients/phoenix-salons/call-log.md`)
|
||||
**Applies to:** all clients, all audit-adjacent calls
|
||||
|
||||
---
|
||||
|
||||
## Operating rules (binding)
|
||||
|
||||
1. **Closed-loop rule.** If it's not in the script, it's not on the call. The script is generated from the canonical record the day before. No "I'll look that up" items — any item that isn't verified and in the script is deferred to Phase 2.
|
||||
2. **Evidence standard.** On call, state only what has been re-verified in the last 48 hours. Headless-blocked or stale surface → say "I will re-verify before we touch it" and mark it pending in the call log.
|
||||
3. **Hours GATE.** Hours are a gate, not a finding. Day-by-day table with BLANKS where surfaces conflict. Verbal lock of all seven days is required, then written confirmation to the owner within 2 hours, then record update. No written numbers = item stays OPEN. Full stop. No monitoring handoff while hours are OPEN.
|
||||
4. **Pricing.** Either the economics guide's 7-question module is in the script (§3 of `docs/architecture/economics-pricing-execution-guide-v1.md`) or pricing is not discussed. No improvisation.
|
||||
5. **Education is a one-pager, not a call topic.** Concepts the owner didn't know about (GEO, AEO, llms.txt) go in the written follow-up.
|
||||
6. **Cost log.** Minutes spent per fix, recorded in the call log / fix log. Becomes the first real price anchor (Tier 2 validation for the economics model).
|
||||
|
||||
---
|
||||
|
||||
## PHASE 0 — Pre-call (day before, mostly automated)
|
||||
|
||||
1. `multi_scraper` capture, all 4 surfaces → `extractions/<client>_<date>.json`
|
||||
2. Update `docs/clients/<client>/` canonical record: hours table (every day × every surface, **BLANKS where conflict**), old-name list (live-verified or marked "re-verify"), SSR word count, rating/review counts, open items
|
||||
3. Send pre-call owner form (`docs/operations/templates/pre-call-owner-form.md`), 24–48h out
|
||||
4. Generate call script FROM the record. Check the closed-loop rule: every item in the script is verified <48h. GATE check: hours table present with blanks where unresolved.
|
||||
|
||||
## PHASE 1 — Call (~15 min; presentation, not discovery)
|
||||
|
||||
1. **Open:** locked good news (NAP + rating), 30 seconds.
|
||||
2. **Findings** in fixed order: finding → business impact → what we do → owner.
|
||||
3. **Hours GATE:** read the table day by day, blanks visible. Owner gives all seven days. Verbal lock recorded verbatim.
|
||||
4. **Close:** exact next actions with owners and dates.
|
||||
5. **Immediately after (within 2h):** text/email with the locked numbers + education one-pager.
|
||||
6. Call log entry created the same day (convention: `docs/clients/<client>/call-log.md`).
|
||||
|
||||
## PHASE 2 — Fix (the PoC)
|
||||
|
||||
1. Action list from the call; owners assigned at the call.
|
||||
2. Re-capture after every fix. Before/after numbers in the report.
|
||||
3. **Cost log:** minutes per fix → first real price anchor.
|
||||
|
||||
## PHASE 3 — Monitoring (retainer begins)
|
||||
|
||||
Explicit handoff: audit phase ends, monitoring begins. The record defines:
|
||||
- surfaces watched
|
||||
- frequency
|
||||
- alert thresholds
|
||||
|
||||
`multi_scraper` on schedule, diff vs record, drift flagged. **Entry condition: hours item CLOSED with written numbers.** No exception.
|
||||
|
||||
---
|
||||
|
||||
## Lessons from v1 (2026-08-17 Phoenix call)
|
||||
|
||||
- Education inside the findings call was a design error → rule 5.
|
||||
- Hours asked as a question, closed as "verbal lock" with no numbers → rule 3.
|
||||
- Audit timeline mislabeled (June and July audits exist; Aug 13 is not "the original") → timeline lives in the call log, not in call memory.
|
||||
- Owner assignments must be explicit per finding (who fixes, who posts, who just approves).
|
||||
@@ -0,0 +1,25 @@
|
||||
# DR-001: Website Ownership Scope
|
||||
**Status:** LOCKED · 2026-08-16 · Supersedes: nothing
|
||||
|
||||
## Decision
|
||||
1. Phoenix: website JSON-LD (hours) is fixed ONCE under the existing Lovable
|
||||
project access during onboarding cleanup. Logged in the change log as a
|
||||
one-time intervention. We do not claim ongoing website control.
|
||||
2. Until the long-term decision lands, the website surface is an EXPLICIT
|
||||
EXCLUSION from the managed service. All reports label website findings as
|
||||
advisory: "fix handed back to owner/developer."
|
||||
3. Long-term model — decided AFTER the Phoenix onboarding call, BEFORE client
|
||||
#3. Options, in preference order:
|
||||
a. We become technical owner of the site (or the structured-data layer)
|
||||
for clients who want the full service.
|
||||
b. We inject/maintain a small isolated JSON-LD file we control
|
||||
(LLM-friendly pointer).
|
||||
c. Website stays read-only, fix always handed back (weakest — breaks the
|
||||
"off your plate" promise; kept only as fallback).
|
||||
Default expectation: (a) or (b). Option (c) as steady state is technical
|
||||
debt that surfaces in every future client conversation.
|
||||
|
||||
## Consequences
|
||||
- Reports already distinguish advisory vs managed surfaces — no pipeline change.
|
||||
- SOP gains a scope line (see SOP v1.1) so the owner hears the same boundary
|
||||
we enforce in reports.
|
||||
@@ -0,0 +1,17 @@
|
||||
# DR-002: Monitoring Cadence
|
||||
**Status:** LOCKED (defaults) · 2026-08-16 · Implementation deferred to post-call review
|
||||
|
||||
## Decision
|
||||
- Days 0–30 after onboarding: audit every day (high-risk window — aggregators
|
||||
and caches still settling onto new values).
|
||||
- After day 30: weekly.
|
||||
- Exception triggers (escalate to daily until stable for 2 consecutive runs):
|
||||
any change we make, or any surface showing instability.
|
||||
- Client communication: NO per-drift alerts. The trust receipt / monthly
|
||||
summary is the only client-facing monitoring surface. The client should feel
|
||||
watched, not pinged.
|
||||
|
||||
## Deferred (post-call)
|
||||
- Actual cron wiring of audit_pipeline.sh at these intervals.
|
||||
- Escalation detection logic (what counts as "instability").
|
||||
- First real onboarding must exist before any of this is built.
|
||||
@@ -0,0 +1,43 @@
|
||||
# DR-003: Website Build-Stack Fingerprinting + SSR Signal
|
||||
|
||||
**Date:** 2026-08-17
|
||||
**Status:** Accepted
|
||||
**Supersedes:** nothing (extends v1.1 capture contract)
|
||||
|
||||
## Context
|
||||
|
||||
The Phoenix Salon + Spa audit (2026-08-17) shipped a finding — "no street
|
||||
address on /contact" — that was false: the address exists but is rendered
|
||||
client-side, so the pipeline's plain-HTTP capture did not see it and downstream
|
||||
analysis could not distinguish "content missing" from "content present but
|
||||
JS-rendered." The site was built on Lovable (Vite SPA behind Cloudflare); the
|
||||
capture did not record that, so the fix path ("one-time build fix" vs "edit
|
||||
content") had to be discovered by hand.
|
||||
|
||||
## Decision
|
||||
|
||||
1. `multi_scraper.py` records, at website capture time:
|
||||
- `website.stack.detected` / `website.stack.primary` — build-stack
|
||||
fingerprint (Lovable, v0, Next.js, Nuxt, Gatsby, Vite, React/Angular/Vue/
|
||||
Svelte, WordPress, Squarespace, Wix, Webflow, Shopify, hosts).
|
||||
- `website.stack.html_word_count` / `website.stack.ssr_signal` — word count
|
||||
a non-JavaScript crawler reads from the raw HTML (`weak` < 50 words).
|
||||
2. SOP v1.2: the report states the build stack when known and flags weak SSR;
|
||||
capture-based website findings must be qualified by SSR signal.
|
||||
3. `multi_scraper.py --selftest` covers fingerprinting (Lovable fixture, weak
|
||||
vs present SSR) and runs in CI-free environments without network.
|
||||
|
||||
## Rationale
|
||||
|
||||
Knowing how a site is built determines (a) what the capture can and cannot
|
||||
see, and (b) the cheapest correct fix. Regex fingerprinting on raw HTML is
|
||||
deliberate: known-site audits need a label, not a scanner — false negatives
|
||||
land in `unknown`, never a confident wrong answer.
|
||||
|
||||
## Verification
|
||||
|
||||
- `--selftest`: PASS (Lovable fixture → primary=lovable, ssr=weak; 80-word
|
||||
page → ssr=present, primary=unknown).
|
||||
- Live re-capture of phoenixcameronpark.com: `detected=['lovable']`,
|
||||
`html_word_count=13`, `ssr_signal=weak` — matches the manual Playwright
|
||||
audit of the same day.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Owner Onboarding SOP — 15-Minute Authority Transfer
|
||||
**Status:** Locked v1.1 · 2026-08-16 (scope per DR-001, cadence per DR-002)
|
||||
**Applies to:** Every VeriPath client, after first audit delivery, before monitoring/remediation loop starts.
|
||||
**Operator:** VeriPath (Leonard or Tony). Owner is a participant, not a student — she does 2 things: confirms facts, grants access.
|
||||
|
||||
## The three outcomes (definition of done)
|
||||
1. **Ground truth locked** — owner confirms hours + critical facts verbatim; canonical record updated and version-bumped.
|
||||
2. **Minimum access granted** — GBP: VeriPath account added as manager, role *Edit business info* only. Apple: owner claims + verifies her listing via Apple Business Connect (screen-shared) and sets canonical hours. No credentials ever requested.
|
||||
3. **Operating agreement established** — owner understands: from today VeriPath monitors all surfaces against the canonical record and fixes drift. She does not check or maintain listings. Every grant is revocable in one click.
|
||||
|
||||
If all three are done, the monitoring loop may start. If any is incomplete, log the gap in the receipt and the loop starts in **read-only** mode for that surface.
|
||||
|
||||
---
|
||||
|
||||
## P0 — Pre-work (before the call, not part of 15 min)
|
||||
- [ ] Latest audit report + findings on hand (Gitea, `docs/validation/...` or run dir).
|
||||
- [ ] Canonical record current (`docs/clients/{client}/canonical-business-record-*.json`); draft values ready to read back.
|
||||
- [ ] VeriPath GBP account created/ready to receive the manager invite.
|
||||
- [ ] Call booked, 15 min, screen share available (needed for Apple step).
|
||||
- [ ] Print/pull the owner's exact canonical values (hours per day, phone, address, primary category) so nothing is "looked up" mid-call.
|
||||
|
||||
## P1 — Frame (0:00–2:00)
|
||||
Say, roughly verbatim:
|
||||
> "Fifteen minutes, then you're done. Two things: I read you the facts we hold for your business — you tell me where they're wrong, and that's the official record from now on. Then you hand me the keys — Google in about two minutes, Apple in about three, on screen share. After that, if something drifts, that's our problem, not yours. You'll only hear from us when you should."
|
||||
Confirm she has ~15 min and her phone (Apple verification may text/call).
|
||||
|
||||
## P2 — Lock ground truth (2:00–7:00)
|
||||
Read back, one at a time, and get a yes/correction:
|
||||
1. **Hours** — every day of week, stated as "Monday 9 to 6 … Sunday closed." (This is the field that caused the audit findings; read it slowly.)
|
||||
2. **Phone** — the number customers should call.
|
||||
3. **Address** — street, suite, city, ZIP.
|
||||
4. **Primary category + top services** — what the business *is*, in her words.
|
||||
5. **Anything seasonal/irregular** — holidays, half-day Saturdays, "we close early on Fridays in summer." Capture as notes.
|
||||
|
||||
Rules:
|
||||
- Her correction wins, always. Never argue from scrape data.
|
||||
- Log each confirmation as spoken: field → confirmed value → timestamp.
|
||||
- **Update the canonical record before moving on** (see Update Protocol). If hours changed, the Apple step in P3 uses the new values — not the old ones.
|
||||
|
||||
## P3 — Grant access (7:00–12:00)
|
||||
|
||||
### 3a. Google Business Profile — manager invite (~2 min, she does it)
|
||||
She, signed into her own Google account at `business.google.com`:
|
||||
1. Select the business (e.g. Phoenix Salon + Spa).
|
||||
2. Pencil icon → **Edit business information** (left sidebar) → **People and permissions**.
|
||||
3. **Add people** → type the VeriPath account email (hand it to her, don't make her type it).
|
||||
4. Role: **Edit business info** — explicitly *not* "Manage." Say: "You keep full ownership. I get the smallest key that opens the door I need — edit info only, and you can pull it anytime."
|
||||
5. Send. VeriPath accepts the invite from the email (or it appears under People and permissions as pending) — operator confirms acceptance on screen if she wants to see it.
|
||||
|
||||
Reversibility line (say it): "To revoke: same page, click your account, Remove. One click, no phone call to Google."
|
||||
|
||||
### 3b. Apple — owner claims & verifies (screen share, ~3 min)
|
||||
We cannot do this for her — Apple gates it on identity. Keep it honest and fast:
|
||||
1. Screen share. She opens **abc.apple.com** (Apple Business Connect), signs in with her Apple ID (or creates one — 1 min).
|
||||
2. **Claim a business** → search "{Business Name}" → select the listing.
|
||||
3. **Verify ownership** — prefer **video verification** (fastest); phone-call code is the fallback. Postcard = slow path, log as pending.
|
||||
4. Once claimed, the listing opens in ABC. She sets hours **from the canonical values you hand her** (read them: "Mon–Fri 9 to 6, Saturday 9 to 6, Sunday closed") — operator watches each field, confirms each day.
|
||||
5. Done. Say: "That was the one thing only you could do. It's done. Apple Maps will reflect this; if Apple's review lags a day or two, that's Apple, and we'll watch it."
|
||||
|
||||
UI labels in ABC drift occasionally — follow the flow, not the exact wording. If the claim fails or the listing isn't found: fallback = **Suggest an edit** from Apple Maps on her listing (Apple reviews it, takes days) → log as pending, loop runs read-only on Apple until it lands.
|
||||
|
||||
### 3c. Website — no access grant at onboarding (per DR-001)
|
||||
We do NOT take website admin access during onboarding.
|
||||
- If we already hold project access (Phoenix: Lovable): fix the JSON-LD once as a one-time intervention, log it in the change log. Say so out loud: "We've fixed the hours markup on the site. That was a one-time fix — we don't control the site, and we're not pretending we do."
|
||||
- If we do not hold access: hand the fix back with the exact change needed, and mark the website surface advisory in all future reports.
|
||||
Either way the owner hears the same scope line as the reports.
|
||||
|
||||
## P4 — Operating agreement + close (12:00–15:00)
|
||||
Say, roughly:
|
||||
> "Here's where we are. The record we just locked is the source of truth. We check Google, Apple, Bing, and your website against it on a schedule. When something drifts, we fix it — Google directly, Apple through your listing, website through you or whoever runs it — and you get a short note saying what we found and what we did. You don't check anything. If you ever want out, you remove us from Google in one click and close Apple Business Connect. Nothing here is a trap door."
|
||||
**Scope (say it verbatim):**
|
||||
> "We keep the business listing surfaces correct — Google, Apple, and Bing. The website is advisory: we tell you and your developer exactly what to change. If you want us to own the site's structured data going forward, that's a separate conversation we'll have."
|
||||
|
||||
Then:
|
||||
- [ ] Hand her the **trust receipt** (file, emailed) — what was confirmed, what access was granted, how to revoke each.
|
||||
- [ ] State the first monitoring cycle date.
|
||||
- [ ] Close. No upsell, no tutorial.
|
||||
|
||||
## Monitoring (default cadence — DR-002)
|
||||
- Days 0–30 after onboarding: daily audits (aggregators/caches still settling).
|
||||
- After day 30: weekly.
|
||||
- After any change we make, or any unstable surface: daily until 2 consecutive clean runs.
|
||||
- The owner receives the trust receipt / monthly summary only. **No per-drift alerts.**
|
||||
- Implementation (cron wiring, instability trigger) is deferred until after the first live onboarding — policy is locked, mechanism is not built yet.
|
||||
|
||||
## Update Protocol — canonical record (after P2)
|
||||
For every owner-confirmed field:
|
||||
- `status: "verified"`, `evidence_tier: "tier_1"`, `verified_at: <now>`, `fresh_until: <now + 30d>`
|
||||
- `sources`: append `"owner-confirmed YYYY-MM-DD (onboarding)"`
|
||||
- `record_version` bump (0.1.x → 0.2.0 on first onboarding lock), `system.updated_at` = now, `system.status` → `"active"` when all P2 fields are verified (otherwise stays `draft`)
|
||||
- Commit to Gitea `Tony_tech/veripath` with message: `onboarding: owner-confirmed ground truth for {client} ({date})`
|
||||
|
||||
## Trust receipt (evidence of the transfer)
|
||||
Written to `docs/clients/{client}/ops/onboarding-receipt-YYYY-MM-DD.json`, committed with the canonical update, and emailed to the owner:
|
||||
```json
|
||||
{
|
||||
"client_id": "VP-001",
|
||||
"date": "YYYY-MM-DDT..Z",
|
||||
"operator": "name",
|
||||
"confirmed_fields": { "hours": {"monday": "09:00-18:00", "...": "..."}, "phone": "...", "address": "...", "category": "..." },
|
||||
"access": {
|
||||
"google_business_profile": { "granted_to": "veripath@...com", "role": "edit_business_info", "granted_at": "..." },
|
||||
"apple": { "claimed_by": "owner", "verified_via": "video", "hours_set": true }
|
||||
},
|
||||
"revocation": {
|
||||
"google_business_profile": "business.google.com → business → Edit business information → People and permissions → Remove",
|
||||
"apple": "abc.apple.com → remove team members / close Apple Business Connect"
|
||||
},
|
||||
"gaps": [ "apple verification pending — postcard sent" ]
|
||||
}
|
||||
```
|
||||
Any incomplete outcome **must** appear in `gaps`. No silent partial access.
|
||||
|
||||
## Edge cases
|
||||
- **She is not the GBP owner** → stop 3a. Note in gaps. Either the real owner does 3a, or loop runs read-only on Google until it happens. Never work around ownership.
|
||||
- **Apple verification pending (postcard)** → log in gaps; set hours as soon as claimed; loop read-only on Apple meanwhile.
|
||||
- **Hours corrected in P2** → canonical updated first; Apple step uses corrected values (P2→P3 order is load-bearing).
|
||||
- **Website JSON-LD drift** → 3c handles it. We don't take website admin by default; if we hold project access we fix it once (one-time intervention, logged), otherwise the fix is handed back and the surface stays advisory.
|
||||
- **Owner pushes back on access** → that's a trust conversation, not a checklist. Drop to read-only loop for the withheld surface, log the gap, and do not pressure. The loop still adds value on the surfaces she does grant.
|
||||
|
||||
## Pattern notes (for the next client)
|
||||
Everything client-specific is a placeholder: `{business}`, `{client_id}`, `{veripath_email}`, canonical values pulled from that client's record. The only surface-specific branches are the Apple claim (identity-gated, always owner-done) and GBP invite (always owner-done). If a future surface requires a new access type, add a `3c` step + a revocation line + a receipt field. Nothing else changes.
|
||||
|
||||
## Appendix — Phoenix Salon + Spa (first run)
|
||||
- Canonical values to read back: Mon–Sat **9 AM–6 PM**, Sun **closed**; phone (530) 350-9197; 3460 Robin Ln, Ste 4, Cameron Park, CA 95682; category: beauty salon/spa.
|
||||
- Known drift to fix in the loop after onboarding: Apple Maps shows 10 AM–6 PM all 7 days incl. Sunday (owner sets correct hours during 3b); website JSON-LD closes at 7 PM Mon–Fri, 5 PM Sat/Sun, open Sunday (3c: one-time Lovable fix, logged).
|
||||
- GBP already clean per 2026-08-16 audit — 3a is grant-only, no data fix.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Pre-Call Owner Form (standard template)
|
||||
|
||||
**Client:** {client}
|
||||
**Call date:** {date}
|
||||
**Send:** 24–48h before the call. Owner answers; her answers feed the call script (Phase 0, step 4).
|
||||
|
||||
---
|
||||
|
||||
## 1. Hours (the gate — every day, no ranges)
|
||||
|
||||
| Day | Opens | Closes | Open/Closed |
|
||||
|---|---|---|---|
|
||||
| Monday | | | |
|
||||
| Tuesday | | | |
|
||||
| Wednesday | | | |
|
||||
| Thursday | | | |
|
||||
| Friday | | | |
|
||||
| Saturday | | | |
|
||||
| Sunday | | | |
|
||||
|
||||
If any day varies by season or staff, note it here: ________________
|
||||
|
||||
## 2. Booking
|
||||
|
||||
- How do customers book? (Vagaro link / phone / walk-in / other): ________________
|
||||
- Do you want to be found by "near me" searches? yes / no
|
||||
|
||||
## 3. What matters most
|
||||
|
||||
- Which pages on the website matter most to you? (e.g. services, booking, contact): ________________
|
||||
|
||||
## 4. Known issues
|
||||
|
||||
- Anything you already know is wrong or broken that we should check? ________________
|
||||
|
||||
## 5. What's a win for you
|
||||
|
||||
- In your own words, what would make this call worth your time? ________________
|
||||
|
||||
---
|
||||
|
||||
**Owner notes (optional):**
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Visibility Stack Roadmap — SEO / AEO / GEO Integration
|
||||
**Status:** Frame LOCKED · 2026-08-16 · Signal source: Mohart framework post (x.com/natan_mohart/status/2088994329272652168) + concurrent llms.txt / agent-readable-data discourse
|
||||
**Rule of this doc:** every phase has a hard trigger. No phase starts on vibes.
|
||||
|
||||
## Positioning (the sell)
|
||||
VeriPath keeps a business's verified facts consistent across every surface — so both
|
||||
search engines and AI systems (ChatGPT, Gemini, Claude, Perplexity) represent the
|
||||
business accurately.
|
||||
- **SEO** — found by search engines (GBP, Bing, listings) — *built and in production.*
|
||||
- **AEO** — chosen as the direct answer (structured data, JSON-LD, NAP) — *partially built; website scope is client-negotiated per DR-001.*
|
||||
- **GEO** — recommended/cited by AI systems (consistent, machine-digestible verified truth) — *the canonical record IS the foundation; no GEO product exists yet, by design.*
|
||||
|
||||
Owner-facing one-liner (Ashley call and first 2–3 clients):
|
||||
> "We keep your facts consistent so the systems that recommend you have the right information."
|
||||
|
||||
Acronym framing (SEO/AEO/GEO) is for partners, chambers, and later-stage sales —
|
||||
never the first thing an owner hears.
|
||||
|
||||
## Phase 0 — NOW (in progress, zero code)
|
||||
**Trigger:** none — active.
|
||||
- Three-layer language in calls and sales conversations (above).
|
||||
- Trust receipt carries one paragraph: surfaces kept consistent for both search
|
||||
engines and AI recommendation. (Prose artifact — does not touch the locked
|
||||
beta report format.)
|
||||
- Canonical record remains the single source of truth; all surfaces measured against it.
|
||||
**Exit:** first trust receipt sent.
|
||||
|
||||
## Phase 1 — After first loop closed
|
||||
**Trigger:** Phoenix loop complete (onboarding → Lovable fix → re-run → trust
|
||||
receipt sent → monitoring day 0).
|
||||
- **P1a.** Report format bump v1.1: tag high-severity consistency findings
|
||||
(hours / schema / NAP) as affecting both search and AI recommendation.
|
||||
First change to the locked beta format — deliberate, post-proof.
|
||||
- **P1b.** Decision: lightweight GEO check enters the standard receipt or not.
|
||||
Test = fixed local prompt set (e.g., "best salon in Cameron Park"), manual or
|
||||
scripted verification, against the canonical record. Decide from real owner
|
||||
feedback + whether the check produced anything the owner cared about.
|
||||
**Exit:** format v1.1 committed; P1b decision recorded (DR).
|
||||
|
||||
## Phase 2 — After 2–3 clients onboarded, monitoring running
|
||||
**Trigger:** ≥3 clients with active monitoring clocks.
|
||||
- llms.txt / website structured data as a **client-negotiated website scope**
|
||||
(never silent website access — DR-001 holds).
|
||||
- If P1b check proved signal: standardize it in the receipt and monitoring
|
||||
cadence (DR-002 mechanism absorbs it).
|
||||
- Entity-clarity pass in intake: name/brand variants, disambiguation fields,
|
||||
only if real clients showed the problem.
|
||||
**Exit:** one client with a website-scope deliverable shipped (or explicit
|
||||
evidence it's not wanted → document, move on).
|
||||
|
||||
## Phase 3 — After unit economics proven
|
||||
**Trigger:** 8–12 clients, monitoring at minimal daily attention, proven
|
||||
per-client margin.
|
||||
- Productize the visibility layer as its own tier (the "recommendable by AI"
|
||||
offering) with its own pricing and the GEO check as the measurable artifact.
|
||||
- Partner/chamber channel uses full SEO/AEO/GEO framing.
|
||||
|
||||
## Standing rules (binding, all phases)
|
||||
1. **Verified truth before visibility.** No visibility artifact ships before the
|
||||
client's canonical record exists and is owner-verified.
|
||||
2. **Canonical record is the single source of truth.** Anything agent-readable
|
||||
(llms.txt, schema, GEO check output) is a *rendering* of it — never a new
|
||||
source. Any proposed artifact answers yes/no: does it render verified truth,
|
||||
or create a second truth?
|
||||
3. **No second source, no new surface without a client who asked for it.**
|
||||
4. **No GEO score, no GEO promise.** We sell the *outcome* — facts accurate and consistent everywhere the systems that recommend a business read them. We never promise an "AI visibility score", a ranking movement, or a recommendation share; a score we cannot compute or control is a liability, not a feature.
|
||||
5. **No GEO measurement build.** No AI-answer scraping, prompt-test harnesses, or visibility dashboards until a phase gate fires — and then only against a real, sent trust receipt.
|
||||
6. DR-001 (website scope) and DR-002 (cadence) stand unchanged unless explicitly superseded.
|
||||
|
||||
## Non-goals (until Phase 1 trigger fires)
|
||||
- No llms.txt or website deliverable beyond the DR-001 one-time Lovable fix.
|
||||
- No scraping/monitoring of AI answers; no "AI visibility score."
|
||||
- No changes to the locked beta report format (tags/labels/sections).
|
||||
- No new schema, tiers, or entity graphs beyond the canonical record.
|
||||
- No monitoring implementation before a trust receipt exists.
|
||||
@@ -1,13 +1,13 @@
|
||||
# Digital Operations Partner (DOP) - High-Level Project Plan
|
||||
# VeriPath - High-Level Project Plan
|
||||
|
||||
Last Updated: August 10, 2026
|
||||
Last Updated: August 17, 2026
|
||||
Status: Pre-Build / Blueprint Phase
|
||||
Current Stage: Foundation Locked, Ready for Blueprinting
|
||||
|
||||
---
|
||||
|
||||
## 1. Project Objective
|
||||
To build and launch a differentiated marketing and operating system for Digital Operations Partner (DOP) that solves "Silent Customer Loss" for local businesses. DOP will leverage a "Cold Audit → Threat Diagnosis → Focused PoC → Retainer" commercial motion, combining agent-assisted analysis with strict human oversight to improve upon the narrower, "done-for-you AI search visibility" model seen in competitors like Bizl.com.
|
||||
To build and launch a differentiated marketing and operating system for VeriPath that solves "Silent Customer Loss" for local businesses. VeriPath will leverage a "Cold Audit → Threat Diagnosis → Focused PoC → Retainer" commercial motion, combining agent-assisted analysis with strict human oversight to improve upon the narrower, "done-for-you AI search visibility" model seen in competitors like Bizl.com.
|
||||
|
||||
## 2. Core Positioning (Locked)
|
||||
* The Problem: Silent Customer Loss (Discovery Failure + Customer Path Leakage).
|
||||
@@ -30,7 +30,7 @@ To build and launch a differentiated marketing and operating system for Digital
|
||||
|
||||
### Phase 1: Blueprint & Strategy Lock (Current Phase)
|
||||
*Goal: Finalize the structural framing before any code or complex design work begins.*
|
||||
- [ ] Homepage Framing: Draft the remaining homepage sections (e.g., Proof/Social Proof, The DOP Difference/Methodology, Final CTA).
|
||||
- [ ] Homepage Framing: Draft the remaining homepage sections (e.g., Proof/Social Proof, The VeriPath Difference/Methodology, Final CTA).
|
||||
- [ ] Asset Strategy: Decide how the 5 completed live Cold Audits will be presented to prospects (e.g., gated PDF reports, interactive web dashboards, or narrative case studies).
|
||||
- [ ] Brand & Identity: Lock down the final brand name confirmation, visual direction, and design system fundamentals.
|
||||
- [ ] Tech Stack Decision: Choose the implementation path (e.g., Next.js/React + Headless CMS, Webflow, Framer, or purely static HTML) that best supports the audit intake mechanism.
|
||||
@@ -59,7 +59,7 @@ To build and launch a differentiated marketing and operating system for Digital
|
||||
---
|
||||
|
||||
## 5. Evidence Governance & Human Oversight
|
||||
*The DOP trust model — operationalized human supervision over agent-assisted findings.*
|
||||
*The VeriPath trust model — operationalized human supervision over agent-assisted findings.*
|
||||
* **The Evidence Hierarchy:** All findings are classified as either Tier 1 (Verified) or Tier 2 (Indicative).
|
||||
* **The Fallback Protocol:** If a proposed Tier 1 classification is rejected by human review, it automatically defaults to Tier 2, *unless* the human reviewer explicitly discards the finding as noise or requests more evidence from the agent-assisted backend. No unverified or rejected finding is ever presented to a client as Tier 1.
|
||||
* **Sign-off:** Every client-facing finding requires explicit human approval.
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Client Audit Delivery Email Template
|
||||
|
||||
**Subject:** Verified Online Presence Audit — [Business Name]
|
||||
|
||||
**Body:**
|
||||
|
||||
Hello [Client Name],
|
||||
|
||||
Attached is the VeriPath online presence audit for **[Business Name]**.
|
||||
|
||||
We reviewed your primary customer-facing directories. The audit identifies any inconsistencies that could reduce discoverability or customer confidence.
|
||||
|
||||
**Key findings** are marked with brief recommendations. Please review the attached report when convenient. If you would like help correcting any of the identified items, reply to this email and we can discuss next steps.
|
||||
|
||||
Best,
|
||||
|
||||
Leonard
|
||||
VeriPath
|
||||
|
||||
---
|
||||
|
||||
**Attachments:**
|
||||
- Executive_Report.pdf
|
||||
|
||||
---
|
||||
|
||||
*This template may be reused for future beta client deliveries.*
|
||||
@@ -0,0 +1,75 @@
|
||||
# Crystal Blue Plumbing Heating & Air
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** , CA
|
||||
**Vertical:** Local Services
|
||||
**Audit date:** 14 August 2026
|
||||
**Prepared for:** Crystal Blue Plumbing Heating & Air
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited Crystal Blue Plumbing Heating & Air across its primary online surfaces: Apple Maps. The business shows 4.2 ★ across 65 Google reviews.
|
||||
**2 material integrity issues were identified:**
|
||||
|
||||
1. No website could be located for this business.
|
||||
2. No business description is set on any directory.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✗ | capture failed — excluded from this audit | — |
|
||||
| Apple Maps | ✓ | 4.2 ★ | 65 |
|
||||
| Bing Places | ✗ | capture blocked (bot-wall) — excluded, not verified | — |
|
||||
| Website | ✗ | not found / unreachable at audit time | — |
|
||||
|
||||
> **Limitation:** The website could not be reached or located at audit time. A blocked or missing website is itself a customer-path risk: visitors arriving from search cannot engage the business. Reachability should be re-verified; if the block persists, treat it as a material finding in the next run.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: No website found
|
||||
|
||||
**Observation:**
|
||||
no website listed on Google Business Profile or found on other surfaces
|
||||
|
||||
**Impact:**
|
||||
Without a website, the business cedes its owned property: no service pages, no schema, no conversion path beyond a phone call. AI search surfaces increasingly require a website to cite.
|
||||
|
||||
**Recommendation:**
|
||||
Stand up (or link on GBP) a business website with service pages, contact details, and LocalBusiness schema.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: No business description found on any surface
|
||||
|
||||
**Observation:**
|
||||
no description on any checked surface — weakens local-pack snippet
|
||||
|
||||
**Impact:**
|
||||
An empty description leaves the local-pack snippet to algorithmic default and forfeits the business's own service language in search results.
|
||||
|
||||
**Recommendation:**
|
||||
Write a 2-3 sentence GBP description covering services, service area, and differentiators.
|
||||
|
||||
---
|
||||
|
||||
## Verification Pending
|
||||
|
||||
- **No Google Maps URL found — listing may be unclaimed** — unclassified — reviewer to decide: {}
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; ratings and counts only.
|
||||
- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*
|
||||
@@ -1,53 +1,35 @@
|
||||
# Validation Run: Crystal Blue Plumbing Heating & Air
|
||||
|
||||
**Date:** 2026-08-14
|
||||
**Date:** 14 August 2026
|
||||
**Business:** Crystal Blue Plumbing Heating & Air
|
||||
**Location:** Placerville, CA (search) — resolved to Loomis, CA (same metro)
|
||||
**Vertical:** Plumbing / HVAC (GTM-aligned)
|
||||
**Run context:** Post-fix A validation — GBP entity-match gate + phone glyph strip.
|
||||
**Rating:** 4.2 ★ | **Reviews:** 65
|
||||
**Engine findings:** 5 | **Material (in report):** 2 | **Rejected FP/low-signal:** 1 | **Needs verification:** 1
|
||||
|
||||
## Purpose
|
||||
Prove the GBP scraper no longer ingests a wrong entity and that phone/address
|
||||
PUA glyphs (`\ue0b0` map-pin) are stripped before the contract is built.
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
## Surface Results
|
||||
### Material (survive normalization → in REPORT-final.md)
|
||||
|
||||
| Surface | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Google Business Profile | ⚠️ Rejected | This run the gate rejected GBP's matched entity (`name="Hours"`) and fell back to Apple. No glyph pollution. |
|
||||
| Apple Maps | ✅ Good | Primary source: full NAP, 4.2★/65, hours, website, phone `(916) 571-6448` — glyph-stripped. |
|
||||
| Bing Places | ❌ Failed | No data returned (known gap). |
|
||||
| Website | ❌ Failed | HTTP 403 (crystalblueplumbing.com) — distinct from "no website". |
|
||||
- **high** — No website found
|
||||
`no website listed on Google Business Profile or found on other surfaces`
|
||||
- **medium** — No business description found on any surface
|
||||
`no description on any checked surface — weakens local-pack snippet`
|
||||
|
||||
## Fix Verification (Step #2)
|
||||
- **Phone glyph:** raw GBP/Apple phone fields contained a leading `\ue0b0`
|
||||
(BMP Private-Use-Area map-pin). Old strip regex only covered the
|
||||
Supplementary PUA plane (`\U000E0000-\U000EFFFF`), so it never matched.
|
||||
New regex `[\ue000-\uf8ff\U000E0000-\U000EFFFF]` strips both planes.
|
||||
Confirmed clean: phone `'(916) 571-6448'` — `glyph? False`.
|
||||
- **Match gate:** `_looks_like_match()` rejects search-page titles and any
|
||||
name sharing no ≥4-char token with the query. Crystal Blue's
|
||||
`name="Hours"` was rejected; `RK Mechanical Air Services Inc.` (Aire Serv
|
||||
probe) was also rejected. No false positives.
|
||||
### Needs verification (excluded from report until confirmed)
|
||||
|
||||
## Key Findings
|
||||
Engine produced findings from the merged contract (Apple primary). The earlier
|
||||
hour-mismatch false positive (GBP "Open 24 hours" vs Apple) is now suppressed
|
||||
because GBP was rejected, not merged as primary.
|
||||
- **high** — No Google Maps URL found — listing may be unclaimed
|
||||
`unclassified — reviewer to decide: {}`
|
||||
|
||||
## Reliability
|
||||
- Scraper ran to completion, no crash, valid JSON. ✅
|
||||
- Engine produced findings.json + findings.md. ✅
|
||||
- GBP entity gate engaged correctly. ✅
|
||||
- Apple fallback path produces glyph-clean phone. ✅
|
||||
### Rejected (format false-positives / low-signal / tooling gaps)
|
||||
|
||||
## Artifacts
|
||||
- `crystal_blue_plumbing_heating__air_multi_surface_2026-08-14.json` — raw scrape
|
||||
- `findings.json` — engine output
|
||||
- `findings.md` — markdown report
|
||||
- `VALIDATION.md` — this file
|
||||
- ~~Cannot determine review recency — no dates available in samples~~ — tooling gap: scraper captures no review dates (documented limitation)
|
||||
|
||||
## Verdict
|
||||
Pipeline is production-usable for the next real audits. GBP remains the
|
||||
highest-value surface; when it degrades, the gate + Apple fallback keep the
|
||||
contract clean instead of poisoned.
|
||||
### Enhancement tier (noted, excluded from client report)
|
||||
|
||||
- Price level not set on GBP or Apple Maps
|
||||
|
||||
## Pre-delivery consistency gate (v1.1 §6)
|
||||
- Count match: executive summary states 2; Findings section contains 2 ✅
|
||||
- Evidence present: every cited value exists in findings.json / raw capture ✅
|
||||
- Identity match: name, location, and date match intake record ✅
|
||||
|
||||
*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# Phoenix Salon + Spa
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** Cameron Park, CA
|
||||
**Audit date:** 14 August 2026
|
||||
**Prepared for:** Ashley
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We reviewed Phoenix Salon + Spa across four primary surfaces that customers use to find and contact the business: Google Business Profile, Apple Maps, Bing Places, and the current website. The business name, address, and phone number are consistent across the primary surfaces, and the website is reachable.
|
||||
|
||||
Three material findings require attention: business hours differ across Google Business Profile, Apple Maps, and Bing Places; the most recent available customer review is approximately 115 days old; and the website's structured data lists hours that conflict with Google Business Profile.
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Platform | Status | Key metrics |
|
||||
|---|---|---|
|
||||
| Google Business Profile | Complete | 4.9 stars, 64 reviews |
|
||||
| Apple Maps | Complete | 4.9 stars, 65 reviews |
|
||||
| Bing Places | Complete | 5.0 stars, 65 reviews |
|
||||
| Website (phoenixcameronpark.com) | Live | Reachable; no errors observed |
|
||||
|
||||
## Findings
|
||||
|
||||
### 1. Immediate — Business hours differ across platforms
|
||||
|
||||
#### Observation
|
||||
|
||||
Google Business Profile lists weekday hours as 9:00 AM–6:00 PM and shows the business as closed on Sunday. Apple Maps lists 10:00 AM–6:00 PM every day, including Sunday. Bing Places shows additional variations, including 9:00 AM–7:00 PM on Thursday and 9:00 AM–4:00 PM on Saturday.
|
||||
|
||||
#### Impact
|
||||
|
||||
Customers who check Apple Maps or Bing Places may arrive when the business is closed. Conflicting hours can reduce trust and lead to missed appointments.
|
||||
|
||||
#### Recommendation
|
||||
|
||||
Treat Google Business Profile as the source of truth. Update Apple Maps and Bing Places so the hours match Google exactly: 9:00 AM–6:00 PM weekdays, closed Sunday.
|
||||
|
||||
### 2. High — No recent customer reviews
|
||||
|
||||
#### Observation
|
||||
|
||||
The most recent available review is dated 21 April 2026, approximately 115 days before this audit. No newer reviews appear in the sampled primary-surface data.
|
||||
|
||||
#### Impact
|
||||
|
||||
A review feed that has been quiet for nearly four months may suggest low activity to customers who check ratings before booking.
|
||||
|
||||
#### Recommendation
|
||||
|
||||
Request a Google review after each completed appointment by text or email. One or two new reviews per month is sufficient to keep the profile current.
|
||||
|
||||
### 3. High — Website structured-data hours conflict with Google Business Profile
|
||||
|
||||
#### Observation
|
||||
|
||||
The website's `BeautySalon` JSON-LD lists Monday–Friday hours as 9:00 AM–7:00 PM and Saturday–Sunday hours as 9:00 AM–5:00 PM. Google Business Profile lists weekday hours as 9:00 AM–6:00 PM and Sunday as closed.
|
||||
|
||||
#### Impact
|
||||
|
||||
Search systems and other software may read the website's structured data as an additional source of business hours. The conflict makes it unclear which hours customers should trust and can reinforce incorrect hours in downstream results.
|
||||
|
||||
#### Recommendation
|
||||
|
||||
Update the website's `BeautySalon` JSON-LD to match the approved Google Business Profile hours: 9:00 AM–6:00 PM weekdays, closed Sunday. Keep the visible website hours and structured data aligned.
|
||||
|
||||
## Priority Actions
|
||||
|
||||
1. Update Apple Maps and Bing Places to match Google Business Profile hours.
|
||||
2. Update the website's visible hours and `BeautySalon` JSON-LD to match Google Business Profile.
|
||||
3. Begin a simple post-appointment Google review request process.
|
||||
|
||||
## Closing
|
||||
|
||||
The three findings above are the only material issues identified in this beta review. Addressing them will give customers and search systems one consistent set of business information across the primary surfaces reviewed.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Phoenix Salon + Spa — Online Presence Audit
|
||||
*Cameron Park, CA · Audit date: Aug 14, 2026*
|
||||
|
||||
## Executive Summary
|
||||
Ashley, we checked Phoenix Salon + Spa across the four platforms customers actually use to find and book you — Google Business Profile, Apple Maps, Bing Places, and your website. All four returned complete, consistent data: your business name, address, and phone match everywhere, and your site is live and error-free. Two issues surfaced that directly affect bookings and trust — inconsistent business hours across platforms, and a lack of recent customer reviews. Both are fixable quickly and should be resolved before they quietly cost you appointments.
|
||||
|
||||
## What We Checked
|
||||
| Platform | Status |
|
||||
|---|---|
|
||||
| Google Business Profile | ✅ Complete (4.9★, 64 reviews) |
|
||||
| Apple Maps | ✅ Complete (4.9★, 65 reviews) |
|
||||
| Bing Places | ✅ Complete (5.0★, 65 reviews) |
|
||||
| Website (phoenixcameronpark.com) | ✅ Live, no errors |
|
||||
|
||||
Your name, address, and phone are consistent across every platform — no fragmentation there. The two findings below are the only gaps.
|
||||
|
||||
## The Two Issues That Matter
|
||||
|
||||
### 1. Your hours don't match across platforms — *Immediate*
|
||||
Google says you're open **9 AM–6 PM weekdays and closed Sunday**. Apple Maps shows **10 AM–6 PM every day, including Sunday**. Bing shows yet another set (e.g. 9–7 Thursday, 9–4 Saturday). A customer checking Apple on a Sunday at 11 AM thinks you're open — you're not.
|
||||
|
||||
**What this means for you, Ashley:** When a customer sees different hours on different apps, they don't assume the app is wrong — they assume *your business is disorganized*, or they show up to a locked door. That's a lost appointment and a dent in trust, repeated every week. Your Google Business Profile is the single source of truth, and Apple Maps and Bing Places should both be updated to match it exactly — those are the correct hours.
|
||||
|
||||
### 2. No fresh reviews in 115 days — *High*
|
||||
Your most recent review is from **April 21, 2026**. Nothing newer.
|
||||
|
||||
**What this means for you, Ashley:** Reviews are the first thing a new client checks before booking. A feed that goes silent for nearly four months reads as "is this place still active?" — exactly when you're trying to convert a first-time visitor. Steady recent reviews are the cheapest trust signal you have.
|
||||
|
||||
## Your Two Highest-Leverage Fixes
|
||||
1. **Sync your hours to Google.** Google Business Profile is the source of truth — update Apple Maps and Bing Places to match it exactly (9–6 weekdays, closed Sunday). Do it once in each dashboard — takes ~10 minutes and stops the wrong-hours problem permanently.
|
||||
2. **Start a post-appointment review ask.** A simple text or email after each visit requesting a Google review. Even 1–2 new reviews a month keeps your profile looking active and trustworthy.
|
||||
@@ -0,0 +1,144 @@
|
||||
# superior plumbing,heating and air conditioning
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** Rancho Cucamonga, CA
|
||||
**Vertical:** Plumbing
|
||||
**Audit date:** 14 August 2026
|
||||
**Prepared for:** superior plumbing,heating and air conditioning
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited superior plumbing,heating and air conditioning across its primary online surfaces: Google Business Profile, Apple Maps, Bing Places and Website. The business shows 2.7 ★ across 57 Google reviews.
|
||||
**7 material integrity issues were identified:**
|
||||
|
||||
1. Phone numbers are inconsistent across directories.
|
||||
2. Address details differ between Google and Apple.
|
||||
3. Business hours are inconsistent between Google and Apple.
|
||||
5. The website URL does not align across directories.
|
||||
4. Average rating is below the 4.0 trust threshold.
|
||||
6. Ratings vary across surfaces.
|
||||
7. The website lacks LocalBusiness structured data.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✓ | 2.7 ★ | 57 |
|
||||
| Apple Maps | ✓ | No rating | No reviews |
|
||||
| Bing Places | ✓ | 4.5 ★ | 10 |
|
||||
| Website | ✓ | — | — |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Phone number inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists **(909) 319-0746**. Apple lists **+1 (916) 316-5708**. Bing lists **(215) 416-6557**. Website lists **9093190746**.
|
||||
|
||||
**Impact:**
|
||||
Customers reaching the business through a single directory may call a disconnected or wrong number. Inconsistent NAP (name-address-phone) signals reduce local search ranking across all surfaces.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the correct primary number, then update every directory to match. Keep the verified number identical across Google, Apple, and the website.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: Address inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists “6331 Haven Ave, Rancho Cucamonga, CA 91737”. Apple Maps lists “1150 Sibley St, Folsom, CA 95630”.
|
||||
|
||||
**Impact:**
|
||||
Conflicting addresses split local search signals and can send customers or service vehicles to the wrong location. NAP consistency is a core local-ranking factor.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the canonical street address (including unit) with the business, then align Google and Apple to the same string.
|
||||
|
||||
---
|
||||
|
||||
### Finding 3: Hours mismatch between Google and Apple
|
||||
|
||||
**Observation:**
|
||||
friday: Google “7 AM-7 PM” vs Apple “9:00 AM-12:00 PM”; saturday: Google “7 AM-7 PM” vs Apple “10:00 AM-6:00 PM”; sunday: Google “Closed” vs Apple “10:00 AM-5:00 PM”; monday: Google “7 AM-7 PM” vs Apple “9:00 AM-9:00 PM”; tuesday: Google “7 AM-7 PM” vs Apple “9:00 AM-9:00 PM”; wednesday: Google “7 AM-7 PM” vs Apple “9:00 AM-9:00 PM”; thursday: Google “7 AM-7 PM” vs Apple “9:00 AM-9:00 PM”
|
||||
|
||||
**Impact:**
|
||||
Customers planning a visit see different availability depending on which directory they use. Mismatched hours drive no-shows and lost calls at the edge of the workday.
|
||||
|
||||
**Recommendation:**
|
||||
Verify actual operating hours with the business. Update the non-authoritative surface to match Google's verified schedule, or correct Google if its hours are inaccurate.
|
||||
|
||||
---
|
||||
|
||||
### Finding 4: Website URL inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists `https://www.superiorplumbingac.com/`. Apple Maps lists `https://www.yelp.com/biz/w-and-t-hvac-and-plumbing-solutions-folsom-3`.
|
||||
|
||||
**Impact:**
|
||||
Divergent website URLs split SEO equity and distort analytics attribution; link signals may not consolidate on the canonical domain.
|
||||
|
||||
**Recommendation:**
|
||||
Point every directory at the canonical website URL (https, no tracking parameters) and consolidate redirects so link equity consolidates on one domain.
|
||||
|
||||
---
|
||||
|
||||
### Finding 5: Average rating 3.6 — below 4.0 threshold
|
||||
|
||||
**Observation:**
|
||||
Blended average is **3.6** (Google 2.7, Apple None) — below the 4.0 trust threshold.
|
||||
|
||||
**Impact:**
|
||||
A sub-4.0 average is below the trust threshold for most local verticals and suppresses click-through in the local pack regardless of review volume.
|
||||
|
||||
**Recommendation:**
|
||||
Review the lower-rated reviews for recurring service issues; respond publicly to negative reviews and prioritize the service gaps they describe.
|
||||
|
||||
---
|
||||
|
||||
### Finding 6: Rating varies by 1.8 across surfaces (range: 2.7–4.5)
|
||||
|
||||
**Observation:**
|
||||
Google shows **2.7** ★ while Apple Maps shows **None** ★.
|
||||
|
||||
**Impact:**
|
||||
A rating spread across surfaces undermines the consistency signal local search relies on and can suppress the lower-rated surface in results.
|
||||
|
||||
**Recommendation:**
|
||||
Verify which rating is current; the stale surface usually reflects an unmanaged listing that has stopped receiving reviews.
|
||||
|
||||
---
|
||||
|
||||
### Finding 7: No JSON-LD schema found on website
|
||||
|
||||
**Observation:**
|
||||
{"url": "https://www.superiorplumbingac.com/", "has_jsonld": false}
|
||||
|
||||
**Impact:**
|
||||
Without LocalBusiness structured data, AI assistants and structured search cannot reliably classify or cite the business's services, hours, and area served.
|
||||
|
||||
**Recommendation:**
|
||||
Add LocalBusiness JSON-LD (Dentist/Plumber/Contractor type) with name, address, phone, hours, and areaServed.
|
||||
|
||||
---
|
||||
|
||||
## Verification Pending
|
||||
|
||||
- **Most recent review is 301 days old — no fresh review signal** — unclassified — reviewer to decide: {"newest_review_date": "Oct 17, 2025", "days_ago": 301}
|
||||
- **No canonical URL tag on website** — unclassified — reviewer to decide: {"url": "https://www.superiorplumbingac.com/"}
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; ratings and counts only.
|
||||
- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*
|
||||
@@ -1,55 +1,48 @@
|
||||
# Validation Run: Superior HVAC & Plumbing
|
||||
# Validation Run: superior plumbing,heating and air conditioning
|
||||
|
||||
**Date:** 2026-08-14
|
||||
**Business:** Superior HVAC & Plumbing
|
||||
**Location:** Cameron Park, CA (search) — resolved to Rancho Cucamonga, CA
|
||||
**Vertical:** HVAC / Plumbing (GTM-aligned)
|
||||
**Run context:** Second clean external run (Step B) — 4-surface extract after
|
||||
the GBP match-gate + phone-glyph fixes.
|
||||
**Date:** 14 August 2026
|
||||
**Business:** superior plumbing,heating and air conditioning
|
||||
**Rating:** 2.7 ★ | **Reviews:** 57
|
||||
**Engine findings:** 12 | **Material (in report):** 7 | **Rejected FP/low-signal:** 2 | **Needs verification:** 2
|
||||
|
||||
## Purpose
|
||||
Demonstrate a clean multi-surface run on a distinctive-named business and
|
||||
capture real cross-source NAP divergence for the audit record.
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
## Surface Results
|
||||
### Material (survive normalization → in REPORT-final.md)
|
||||
|
||||
| Surface | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Google Business Profile | ✅ Good | `superior plumbing, heating and air conditioning` — 2.7★/57, full hours, phone `(909) 319-0746` (glyph-stripped), website. |
|
||||
| Apple Maps | ✅ Good | `W&T HVAC & Plumbing Solutions` — null rating/reviews, hours present. |
|
||||
| Bing Places | ✅ Good | `Superior Comfort Heating & Cooling` — 4.5★/10. |
|
||||
| Website | ✅ Good | `https://www.superiorplumbingac.com/` (no 403). |
|
||||
- **immediate** — Phone number inconsistent across 2 surface(s)
|
||||
`distinct numbers: 9093190746, 9163165708`
|
||||
- **immediate** — Address inconsistent across 1 surface(s)
|
||||
`google='6331 Haven Ave, Rancho Cucamonga, CA 91737' vs apple='1150 Sibley St, Folsom, CA 95630'`
|
||||
- **immediate** — Hours mismatch on 7 day(s) — GBP is authoritative
|
||||
`7 day(s) genuinely differ: friday: 7 AM-7 PM vs 9:00 AM-12:00 PM; saturday: 7 AM-7 PM vs 10:00 AM-6:00 PM; sunday: Closed vs 10:00 AM-5:00 PM; monday: 7 AM-7 PM vs 9:00 AM-9:00 PM`
|
||||
- **immediate** — Average rating 3.6 — below 4.0 threshold
|
||||
`average 3.6 (google 2.7 / apple None)`
|
||||
- **high** — Website URL inconsistent across 1 surface(s)
|
||||
`google='https://www.superiorplumbingac.com/' vs apple='https://www.yelp.com/biz/w-and-t-hvac-and-plumbing-solutions-folsom-3' — different domains`
|
||||
- **high** — Rating varies by 1.8 across surfaces (range: 2.7–4.5)
|
||||
`google 2.7 vs apple None`
|
||||
- **high** — No JSON-LD schema found on website
|
||||
`no structured data at https://www.superiorplumbingac.com/`
|
||||
|
||||
## Notable: Genuine Cross-Source Divergence
|
||||
This is NOT a scraper bug — it is real directory fragmentation the pipeline
|
||||
correctly captured:
|
||||
### Needs verification (excluded from report until confirmed)
|
||||
|
||||
- **Three different business names** across GBP / Apple / Bing.
|
||||
- **Rating divergence:** 2.7★ (GBP) vs 4.5★ (Bing) vs null (Apple).
|
||||
- **Phone conflict:** GBP `(909) 319-0746` vs Apple `+1 (916) 316-5708`.
|
||||
- **Hours conflict:** GBP Mon–Sat 7AM–7PM / Sun Closed vs Apple 9AM–9PM /
|
||||
Sun 10AM–5PM.
|
||||
- **Website conflict:** GBP `superiorplumbingac.com` vs Apple Yelp URL.
|
||||
- **high** — Most recent review is 301 days old — no fresh review signal
|
||||
`unclassified — reviewer to decide: {"newest_review_date": "Oct 17, 2025", "days_ago": 301}`
|
||||
- **medium** — No canonical URL tag on website
|
||||
`unclassified — reviewer to decide: {"url": "https://www.superiorplumbingac.com/"}`
|
||||
|
||||
These are exactly the inconsistencies a client audit should surface.
|
||||
### Rejected (format false-positives / low-signal / tooling gaps)
|
||||
|
||||
## Fix Verification
|
||||
- **Phone glyph:** GBP phone came out `(909) 319-0746` with no leading
|
||||
`\ue0b0`. Confirmed `glyph? False`.
|
||||
- **Match gate:** GBP matched the correct `superior plumbing…` entity (shares
|
||||
tokens `superior`, `plumbing`, `heating`/`air`); gate passed.
|
||||
- ~~Review count varies by 47 across surfaces~~ — low-signal: platforms count reviews differently; not an integrity issue
|
||||
- ~~Category varies across surfaces: consumer sector, home service, plumber~~ — low-signal: per-platform taxonomy (Apple 'consumer sector' is its default)
|
||||
|
||||
## Reliability
|
||||
- All 4 surfaces returned data. ✅
|
||||
- Engine flagged name/phone/website/hours mismatches across sources. ✅
|
||||
- No crashes, valid JSON, findings.md generated. ✅
|
||||
### Enhancement tier (noted, excluded from client report)
|
||||
|
||||
## Artifacts
|
||||
- `superior_hvac__plumbing_multi_surface_2026-08-14.json` — raw scrape
|
||||
- `findings.json` — engine output
|
||||
- `findings.md` — markdown report
|
||||
- `VALIDATION.md` — this file
|
||||
- Price level not set on GBP or Apple Maps
|
||||
|
||||
## Verdict
|
||||
Clean 4-surface run. Pipeline treats cross-source disagreement as evidence,
|
||||
not error. Production-usable.
|
||||
## Pre-delivery consistency gate (v1.1 §6)
|
||||
- Count match: executive summary states 7; Findings section contains 7 ✅
|
||||
- Evidence present: every cited value exists in findings.json / raw capture ✅
|
||||
- Identity match: name, location, and date match intake record ✅
|
||||
|
||||
*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Above the Gate Dentistry
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** El Dorado Hills, CA
|
||||
**Vertical:** Dental
|
||||
**Audit date:** 15 August 2026
|
||||
**Prepared for:** Above the Gate Dentistry
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited Above the Gate Dentistry across its primary online surfaces: Google Business Profile and Apple Maps. The business shows 4.9 ★ across 166 Google reviews.
|
||||
**3 material integrity issues were identified:**
|
||||
|
||||
1. Phone numbers are inconsistent across directories.
|
||||
2. No website could be located for this business.
|
||||
3. No business description is set on any directory.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✓ | 4.9 ★ | 166 |
|
||||
| Apple Maps | ✓ | 4.8 ★ | 27 |
|
||||
| Bing Places | ✗ | capture blocked (bot-wall) — excluded, not verified | — |
|
||||
| Website | ✗ | not found / unreachable at audit time | — |
|
||||
|
||||
> **Limitation:** The website could not be reached or located at audit time. A blocked or missing website is itself a customer-path risk: visitors arriving from search cannot engage the business. Reachability should be re-verified; if the block persists, treat it as a material finding in the next run.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Phone number inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists **(916) 299-8983**. Apple lists **+1 (916) 931-1996**.
|
||||
|
||||
**Impact:**
|
||||
Customers reaching the business through a single directory may call a disconnected or wrong number. Inconsistent NAP (name-address-phone) signals reduce local search ranking across all surfaces.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the correct primary number, then update every directory to match. Keep the verified number identical across Google, Apple, and the website.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: Website listed on GBP but unreachable at audit time
|
||||
|
||||
**Observation:**
|
||||
GBP website='http://www.abovethegate.com/' — surface blocked; documented limitation per process §4
|
||||
|
||||
**Impact:**
|
||||
Without a website, the business cedes its owned property: no service pages, no schema, no conversion path beyond a phone call. AI search surfaces increasingly require a website to cite.
|
||||
|
||||
**Recommendation:**
|
||||
Stand up (or link on GBP) a business website with service pages, contact details, and LocalBusiness schema.
|
||||
|
||||
---
|
||||
|
||||
### Finding 3: No business description found on any surface
|
||||
|
||||
**Observation:**
|
||||
no description on any checked surface — weakens local-pack snippet
|
||||
|
||||
**Impact:**
|
||||
An empty description leaves the local-pack snippet to algorithmic default and forfeits the business's own service language in search results.
|
||||
|
||||
**Recommendation:**
|
||||
Write a 2-3 sentence GBP description covering services, service area, and differentiators.
|
||||
|
||||
---
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; ratings and counts only.
|
||||
- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*
|
||||
@@ -0,0 +1,51 @@
|
||||
# Validation Run: Above the Gate Dentistry
|
||||
|
||||
**Date:** 15 August 2026
|
||||
**Business:** Above the Gate Dentistry
|
||||
**Rating:** 4.9 ★ | **Reviews:** 166
|
||||
**Engine findings:** 10 | **Material (in report):** 3 | **Rejected FP/low-signal:** 5 | **Needs verification:** 0
|
||||
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
### Material (survive normalization → in REPORT-final.md)
|
||||
|
||||
- **immediate** — Phone number inconsistent across 1 surface(s)
|
||||
`distinct numbers: 9162998983, 9169311996`
|
||||
- **high** — Website listed on GBP but unreachable at audit time
|
||||
`GBP website='http://www.abovethegate.com/' — surface blocked; documented limitation per process §4`
|
||||
- **medium** — No business description found on any surface
|
||||
`no description on any checked surface — weakens local-pack snippet`
|
||||
|
||||
### Needs verification (excluded from report until confirmed)
|
||||
|
||||
- none
|
||||
|
||||
### Rejected (format false-positives / low-signal / tooling gaps)
|
||||
|
||||
- ~~Address inconsistent across 1 surface(s)~~ — same street/city/zip; suite format only (# vs Ste vs Unit)
|
||||
- ~~Hours mismatch on 5 day(s) — GBP is authoritative~~ — format only: '8 AM' vs '8:00 AM' — identical after parse
|
||||
- ~~Review count varies by 139 across surfaces~~ — low-signal: platforms count reviews differently; not an integrity issue
|
||||
- ~~Cannot determine review recency — no dates available in samples~~ — tooling gap: scraper captures no review dates (documented limitation)
|
||||
- ~~Category varies across surfaces: dentist, health care~~ — low-signal: per-platform taxonomy (Apple 'consumer sector' is its default)
|
||||
|
||||
### Enhancement tier (noted, excluded from client report)
|
||||
|
||||
- Only 0% of sample reviews mention specific services or practitioners
|
||||
- Price level not set on GBP or Apple Maps
|
||||
|
||||
## Pre-delivery consistency gate (v1.1 §6)
|
||||
- Count match: executive summary states 3; Findings section contains 3 ✅
|
||||
- Evidence present: every cited value exists in findings.json / raw capture ✅
|
||||
- Identity match: name, location, and date match intake record ✅
|
||||
|
||||
*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*
|
||||
|
||||
## GATE RESULT
|
||||
|
||||
**PASS** — report_gate.py, 2026-08-15T16:27:11
|
||||
|
||||
- Count: 3 stated / 3 material after review — match
|
||||
- Identity: report title + date match raw capture ('Above the Gate Dentistry', 15 August 2026) — match
|
||||
- Surface disclosure: all captured/failed surfaces rendered correctly — match
|
||||
- Material support: every material finding re-derived from raw capture — hold
|
||||
- Needs-verification open: 0
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"audit": {
|
||||
"timestamp": "2026-08-15T13:52:12.239156+00:00",
|
||||
"tool": "multi_scraper_v1",
|
||||
"surfaces_checked": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": false,
|
||||
"website": false
|
||||
},
|
||||
"verification": {
|
||||
"phone_mismatch": {
|
||||
"primary": "(916) 299-8983",
|
||||
"conflicts": {
|
||||
"apple": "+1 (916) 931-1996"
|
||||
}
|
||||
},
|
||||
"website_mismatch": {
|
||||
"primary": "http://www.abovethegate.com/",
|
||||
"conflicts": {
|
||||
"apple": "http://www.abovethegate.com"
|
||||
}
|
||||
},
|
||||
"hours_mismatch": {
|
||||
"monday": {
|
||||
"google": "7 AM-4 PM",
|
||||
"apple": "7:00 AM-4:00 PM"
|
||||
},
|
||||
"tuesday": {
|
||||
"google": "7 AM-6 PM",
|
||||
"apple": "7:00 AM-7:00 PM"
|
||||
},
|
||||
"wednesday": {
|
||||
"google": "7 AM-5 PM",
|
||||
"apple": "7:00 AM-5:00 PM"
|
||||
},
|
||||
"thursday": {
|
||||
"google": "7 AM-6 PM",
|
||||
"apple": "7:00 AM-6:00 PM"
|
||||
},
|
||||
"friday": {
|
||||
"google": "7 AM-4 PM",
|
||||
"apple": "7:00 AM-4:00 PM"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"primary_source": "google_business_profile",
|
||||
"name": "Above the Gate Dentistry",
|
||||
"address": "4517 Serrano Pkwy #104, El Dorado Hills, CA 95762",
|
||||
"phone": "(916) 299-8983",
|
||||
"website": "http://www.abovethegate.com/",
|
||||
"rating": 4.9,
|
||||
"reviews": 166,
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Dr. Tuttle is very careful and conscious. Very personable and caring. Was able to get in on the same day, but with extra fee, to reapply a crown that came off.",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "I hope you take my advice and go see Dr.Tuttle! The care and expertise is beyond anything I've ever experienced with my many years of a dental issues. I'm so grateful to him and the whole practice. Th",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "Great service. Nice. Attentive. Good combo of tech and old fashioned dentistry. Enjoyed it, and that's saying a lot for a dentist visit. First time there. Insurance was not cooperating, so uncertainty",
|
||||
"rating": 5
|
||||
}
|
||||
],
|
||||
"hours": {
|
||||
"saturday": "Closed",
|
||||
"sunday": "Closed",
|
||||
"monday": "7 AM-4 PM",
|
||||
"tuesday": "7 AM-6 PM",
|
||||
"wednesday": "7 AM-5 PM",
|
||||
"thursday": "7 AM-6 PM",
|
||||
"friday": "7 AM-4 PM"
|
||||
},
|
||||
"category": "Dentist",
|
||||
"price_level": null,
|
||||
"photos_count": null,
|
||||
"description": null,
|
||||
"coordinates": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
},
|
||||
"closed_status": {
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"sources": {
|
||||
"google_business_profile": {
|
||||
"name": "Above the Gate Dentistry",
|
||||
"address": "4517 Serrano Pkwy #104, El Dorado Hills, CA 95762",
|
||||
"phone": "(916) 299-8983",
|
||||
"website": "http://www.abovethegate.com/",
|
||||
"rating": 4.9,
|
||||
"reviews": 166,
|
||||
"hours": {
|
||||
"saturday": "Closed",
|
||||
"sunday": "Closed",
|
||||
"monday": "7 AM-4 PM",
|
||||
"tuesday": "7 AM-6 PM",
|
||||
"wednesday": "7 AM-5 PM",
|
||||
"thursday": "7 AM-6 PM",
|
||||
"friday": "7 AM-4 PM"
|
||||
},
|
||||
"category": "Dentist",
|
||||
"price_level": null,
|
||||
"description": null,
|
||||
"photos_count": null,
|
||||
"latitude": null,
|
||||
"longitude": null,
|
||||
"url": "https://www.google.com/maps/search/Above%20the%20Gate%20Dentistry%20El%20Dorado%20Hills%2C%20CA/data=!3m1!4b1",
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"apple_maps": {
|
||||
"address": "4517 Serrano Pkwy, El Dorado Hills, CA 95762",
|
||||
"hours": {
|
||||
"monday": "7:00 AM-4:00 PM",
|
||||
"friday": "7:00 AM-4:00 PM",
|
||||
"tuesday": "7:00 AM-7:00 PM",
|
||||
"wednesday": "7:00 AM-5:00 PM",
|
||||
"thursday": "7:00 AM-6:00 PM"
|
||||
},
|
||||
"rating": 4.8,
|
||||
"reviews": 27,
|
||||
"coordinates": {
|
||||
"lat": 38.6727479,
|
||||
"lon": -121.0639654
|
||||
},
|
||||
"name": "Above the Gate Dentistry",
|
||||
"category": "Health Care",
|
||||
"phone": "+1 (916) 931-1996",
|
||||
"website": "http://www.abovethegate.com",
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Dr. Tuttle is very careful and conscious. Very personable and caring. Was able to get in on the same day, but with extra fee, to reapply a crown that came off.",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "I hope you take my advice and go see Dr.Tuttle! The care and expertise is beyond anything I've ever experienced with my many years of a dental issues. I'm so grateful to him and the whole practice. Th",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "Great service. Nice. Attentive. Good combo of tech and old fashioned dentistry. Enjoyed it, and that's saying a lot for a dentist visit. First time there. Insurance was not cooperating, so uncertainty",
|
||||
"rating": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"bing_places": null,
|
||||
"website": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"audit_engine": "v1",
|
||||
"timestamp": "2026-08-15T13:52:12.369109+00:00",
|
||||
"business": "Above the Gate Dentistry",
|
||||
"summary": {
|
||||
"total_findings": 10,
|
||||
"severity_counts": {
|
||||
"immediate": 3,
|
||||
"high": 1,
|
||||
"medium": 4,
|
||||
"enhancement": 2
|
||||
},
|
||||
"sub_score_issues": {
|
||||
"Digital Identity": 3,
|
||||
"Google Business": 1,
|
||||
"Technical SEO": 1,
|
||||
"Reviews": 3,
|
||||
"Content": 2
|
||||
},
|
||||
"surfaces_ok": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true
|
||||
},
|
||||
"surfaces_failed": {
|
||||
"bing_places": false,
|
||||
"website": false
|
||||
},
|
||||
"rating": 4.9,
|
||||
"reviews": 166
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "nap_phone_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Phone number inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "(916) 299-8983",
|
||||
"apple": "+1 (916) 931-1996"
|
||||
},
|
||||
"recommendation": "Standardize phone format across all directories. GBP is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "nap_address_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Address inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "4517 Serrano Pkwy #104, El Dorado Hills, CA 95762",
|
||||
"apple": "4517 Serrano Pkwy, El Dorado Hills, CA 95762"
|
||||
},
|
||||
"recommendation": "Standardize exact address format. GBP address is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "hours_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Hours mismatch on 5 day(s) \u2014 GBP is authoritative",
|
||||
"sub_score": "Google Business",
|
||||
"evidence": [
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "7 AM-4 PM",
|
||||
"apple_maps": "7:00 AM-4:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "7 AM-6 PM",
|
||||
"apple_maps": "7:00 AM-7:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "7 AM-5 PM",
|
||||
"apple_maps": "7:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "7 AM-6 PM",
|
||||
"apple_maps": "7:00 AM-6:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "7 AM-4 PM",
|
||||
"apple_maps": "7:00 AM-4:00 PM"
|
||||
}
|
||||
],
|
||||
"recommendation": "Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."
|
||||
},
|
||||
{
|
||||
"id": "no_website",
|
||||
"severity": "high",
|
||||
"title": "No website found",
|
||||
"sub_score": "Technical SEO",
|
||||
"evidence": {},
|
||||
"recommendation": "A website is essential for credibility, schema markup, and conversion. Launch one immediately."
|
||||
},
|
||||
{
|
||||
"id": "review_count_delta",
|
||||
"severity": "medium",
|
||||
"title": "Review count varies by 139 across surfaces",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"google": 166,
|
||||
"apple": 27
|
||||
},
|
||||
"recommendation": "Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership."
|
||||
},
|
||||
{
|
||||
"id": "review_dates_unavailable",
|
||||
"severity": "medium",
|
||||
"title": "Cannot determine review recency \u2014 no dates available in samples",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sources_with_dates": []
|
||||
},
|
||||
"recommendation": "Review date data is not available from current surfaces. Monitor via GBP dashboard directly."
|
||||
},
|
||||
{
|
||||
"id": "category_fragmentation",
|
||||
"severity": "medium",
|
||||
"title": "Category varies across surfaces: dentist, health care",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "dentist",
|
||||
"apple": "health care"
|
||||
},
|
||||
"recommendation": "Align primary category across all directories. GBP primary category is the ranking driver."
|
||||
},
|
||||
{
|
||||
"id": "no_description",
|
||||
"severity": "medium",
|
||||
"title": "No business description found on any surface",
|
||||
"sub_score": "Content",
|
||||
"evidence": {
|
||||
"surfaces_checked": [
|
||||
"google",
|
||||
"bing",
|
||||
"website"
|
||||
]
|
||||
},
|
||||
"recommendation": "Add a keyword-rich description to GBP and website. Descriptions improve click-through and category relevance."
|
||||
},
|
||||
{
|
||||
"id": "reviews_low_specificity",
|
||||
"severity": "enhancement",
|
||||
"title": "Only 0% of sample reviews mention specific services or practitioners",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
},
|
||||
"recommendation": "Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better."
|
||||
},
|
||||
{
|
||||
"id": "no_price_level",
|
||||
"severity": "enhancement",
|
||||
"title": "Price level not set on GBP or Apple Maps",
|
||||
"sub_score": "Content",
|
||||
"evidence": {},
|
||||
"recommendation": "Set price level ($\u2013$$$$) on GBP. Helps users self-select and improves relevance."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
# Audit Findings — Above the Gate Dentistry
|
||||
|
||||
**Generated:** 2026-08-15 13:52 UTC
|
||||
**Sources checked:** google_business_profile, apple_maps
|
||||
**Rating:** 4.9 | **Reviews:** 166
|
||||
|
||||
## Severity Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🟥 Immediate | 3 |
|
||||
| 🟧 High | 1 |
|
||||
| 🟨 Medium | 4 |
|
||||
| 🟩 Enhancement | 2 |
|
||||
| **Total** | **10** |
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding #1: Phone number inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize phone format across all directories. GBP is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "(916) 299-8983",
|
||||
"apple": "+1 (916) 931-1996"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #2: Address inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize exact address format. GBP address is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "4517 Serrano Pkwy #104, El Dorado Hills, CA 95762",
|
||||
"apple": "4517 Serrano Pkwy, El Dorado Hills, CA 95762"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #3: Hours mismatch on 5 day(s) — GBP is authoritative
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Google Business
|
||||
- **Recommendation:** Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss.
|
||||
- **Evidence:** ```json
|
||||
[
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "7 AM-4 PM",
|
||||
"apple_maps": "7:00 AM-4:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "7 AM-6 PM",
|
||||
"apple_maps": "7:00 AM-7:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "7 AM-5 PM",
|
||||
"apple_maps": "7:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "7 AM-6 PM",
|
||||
"apple_maps": "7:00 AM-6:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "7 AM-4 PM",
|
||||
"apple_maps": "7:00 AM-4:00 PM"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Finding #4: No website found
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Technical SEO
|
||||
- **Recommendation:** A website is essential for credibility, schema markup, and conversion. Launch one immediately.
|
||||
|
||||
### Finding #5: Review count varies by 139 across surfaces
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": 166,
|
||||
"apple": 27
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #6: Cannot determine review recency — no dates available in samples
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Review date data is not available from current surfaces. Monitor via GBP dashboard directly.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sources_with_dates": []
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #7: Category varies across surfaces: dentist, health care
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Align primary category across all directories. GBP primary category is the ranking driver.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "dentist",
|
||||
"apple": "health care"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #8: No business description found on any surface
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Add a keyword-rich description to GBP and website. Descriptions improve click-through and category relevance.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"surfaces_checked": [
|
||||
"google",
|
||||
"bing",
|
||||
"website"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #9: Only 0% of sample reviews mention specific services or practitioners
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #10: Price level not set on GBP or Apple Maps
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Set price level ($–$$$$) on GBP. Helps users self-select and improves relevance.
|
||||
|
||||
## Failed Surfaces
|
||||
|
||||
- ❌ `bing_places` — no data returned
|
||||
- ❌ `website` — no data returned
|
||||
@@ -0,0 +1,111 @@
|
||||
# AirTech Pros
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** Sacramento, CA
|
||||
**Vertical:** HVAC
|
||||
**Audit date:** 15 August 2026
|
||||
**Prepared for:** AirTech Pros
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited AirTech Pros across its primary online surfaces: Google Business Profile, Apple Maps and Website. The business shows 4.8 ★ across 1,121 Google reviews.
|
||||
**5 material integrity issues were identified:**
|
||||
|
||||
1. Phone numbers are inconsistent across directories.
|
||||
2. Address details differ between Google and Apple.
|
||||
3. Business hours are inconsistent between Google and Apple.
|
||||
4. The website URL does not align across directories.
|
||||
5. The website lacks LocalBusiness structured data.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✓ | 4.8 ★ | 1,121 |
|
||||
| Apple Maps | ✓ | No rating | No reviews |
|
||||
| Bing Places | ✗ | capture blocked (bot-wall) — excluded, not verified | — |
|
||||
| Website | ✓ | — | — |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Phone number inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists **(530) 303-2225**. Apple lists **+1 (916) 930-0010**.
|
||||
|
||||
**Impact:**
|
||||
Customers reaching the business through a single directory may call a disconnected or wrong number. Inconsistent NAP (name-address-phone) signals reduce local search ranking across all surfaces.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the correct primary number, then update every directory to match. Keep the verified number identical across Google, Apple, and the website.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: Address inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists “10210 Systems Pkwy #370, Sacramento, CA 95827”. Apple Maps lists “4961 Windplay Dr, El Dorado Hills, CA 95762”.
|
||||
|
||||
**Impact:**
|
||||
Conflicting addresses split local search signals and can send customers or service vehicles to the wrong location. NAP consistency is a core local-ranking factor.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the canonical street address (including unit) with the business, then align Google and Apple to the same string.
|
||||
|
||||
---
|
||||
|
||||
### Finding 3: Hours mismatch between Google and Apple
|
||||
|
||||
**Observation:**
|
||||
saturday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; sunday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; monday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; tuesday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; wednesday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; thursday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; friday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”
|
||||
|
||||
**Impact:**
|
||||
Customers planning a visit see different availability depending on which directory they use. Mismatched hours drive no-shows and lost calls at the edge of the workday.
|
||||
|
||||
**Recommendation:**
|
||||
Verify actual operating hours with the business. Update the non-authoritative surface to match Google's verified schedule, or correct Google if its hours are inaccurate.
|
||||
|
||||
---
|
||||
|
||||
### Finding 4: Website URL inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists `https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp`. Apple Maps lists `http://teamairtech.com/`.
|
||||
|
||||
**Impact:**
|
||||
Divergent website URLs split SEO equity and distort analytics attribution; link signals may not consolidate on the canonical domain.
|
||||
|
||||
**Recommendation:**
|
||||
Point every directory at the canonical website URL (https, no tracking parameters) and consolidate redirects so link equity consolidates on one domain.
|
||||
|
||||
---
|
||||
|
||||
### Finding 5: JSON-LD present but no LocalBusiness type
|
||||
|
||||
**Observation:**
|
||||
schema at https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp lacks LocalBusiness/Dentist/Plumber type — AI/structured data can't classify the business
|
||||
|
||||
**Impact:**
|
||||
Without LocalBusiness structured data, AI assistants and structured search cannot reliably classify or cite the business's services, hours, and area served.
|
||||
|
||||
**Recommendation:**
|
||||
Add LocalBusiness JSON-LD (Dentist/Plumber/Contractor type) with name, address, phone, hours, and areaServed.
|
||||
|
||||
---
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; ratings and counts only.
|
||||
- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*
|
||||
@@ -0,0 +1,51 @@
|
||||
# Validation Run: AirTech Pros
|
||||
|
||||
**Date:** 15 August 2026
|
||||
**Business:** AirTech Pros
|
||||
**Rating:** 4.8 ★ | **Reviews:** 1121
|
||||
**Engine findings:** 8 | **Material (in report):** 5 | **Rejected FP/low-signal:** 2 | **Needs verification:** 0
|
||||
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
### Material (survive normalization → in REPORT-final.md)
|
||||
|
||||
- **immediate** — Phone number inconsistent across 2 surface(s)
|
||||
`distinct numbers: 5303032225, 9169300010`
|
||||
- **immediate** — Address inconsistent across 1 surface(s)
|
||||
`google='10210 Systems Pkwy #370, Sacramento, CA 95827' vs apple='4961 Windplay Dr, El Dorado Hills, CA 95762'`
|
||||
- **immediate** — Hours mismatch on 7 day(s) — GBP is authoritative
|
||||
`7 day(s) genuinely differ: saturday: Open 24 hours vs 12:00 AM-12:00 PM; sunday: Open 24 hours vs 12:00 AM-12:00 PM; monday: Open 24 hours vs 12:00 AM-12:00 PM; tuesday: Open 24 hours vs 12:00 AM-12:00 PM`
|
||||
- **high** — Website URL inconsistent across 1 surface(s)
|
||||
`google='https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp' vs apple='http://teamairtech.com/' — different domains`
|
||||
- **medium** — JSON-LD present but no LocalBusiness type
|
||||
`schema at https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp lacks LocalBusiness/Dentist/Plumber type — AI/structured data can't classify the business`
|
||||
|
||||
### Needs verification (excluded from report until confirmed)
|
||||
|
||||
- none
|
||||
|
||||
### Rejected (format false-positives / low-signal / tooling gaps)
|
||||
|
||||
- ~~Cannot determine review recency — no dates available in samples~~ — tooling gap: scraper captures no review dates (documented limitation)
|
||||
- ~~Category varies across surfaces: air conditioning contractor, consumer sector~~ — low-signal: per-platform taxonomy (Apple 'consumer sector' is its default)
|
||||
|
||||
### Enhancement tier (noted, excluded from client report)
|
||||
|
||||
- Price level not set on GBP or Apple Maps
|
||||
|
||||
## Pre-delivery consistency gate (v1.1 §6)
|
||||
- Count match: executive summary states 5; Findings section contains 5 ✅
|
||||
- Evidence present: every cited value exists in findings.json / raw capture ✅
|
||||
- Identity match: name, location, and date match intake record ✅
|
||||
|
||||
*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*
|
||||
|
||||
## GATE RESULT
|
||||
|
||||
**PASS** — report_gate.py, 2026-08-15T16:27:11
|
||||
|
||||
- Count: 5 stated / 5 material after review — match
|
||||
- Identity: report title + date match raw capture ('AirTech Pros', 15 August 2026) — match
|
||||
- Surface disclosure: all captured/failed surfaces rendered correctly — match
|
||||
- Material support: every material finding re-derived from raw capture — hold
|
||||
- Needs-verification open: 0
|
||||
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"audit": {
|
||||
"timestamp": "2026-08-15T13:54:36.440902+00:00",
|
||||
"tool": "multi_scraper_v1",
|
||||
"surfaces_checked": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": false,
|
||||
"website": true
|
||||
},
|
||||
"verification": {
|
||||
"name_mismatch": {
|
||||
"primary": "AirTech Pros",
|
||||
"conflicts": {
|
||||
"apple": "Air Tech HVAC"
|
||||
}
|
||||
},
|
||||
"phone_mismatch": {
|
||||
"primary": "(530) 303-2225",
|
||||
"conflicts": {
|
||||
"apple": "+1 (916) 930-0010"
|
||||
}
|
||||
},
|
||||
"website_mismatch": {
|
||||
"primary": "https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"conflicts": {
|
||||
"apple": "http://teamairtech.com/"
|
||||
}
|
||||
},
|
||||
"hours_mismatch": {
|
||||
"saturday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"sunday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"monday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"tuesday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"wednesday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"thursday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"friday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"primary_source": "google_business_profile",
|
||||
"name": "AirTech Pros",
|
||||
"address": "10210 Systems Pkwy #370, Sacramento, CA 95827",
|
||||
"phone": "(530) 303-2225",
|
||||
"website": "https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"rating": 4.8,
|
||||
"reviews": 1121,
|
||||
"reviews_sample": null,
|
||||
"hours": {
|
||||
"saturday": "Open 24 hours",
|
||||
"sunday": "Open 24 hours",
|
||||
"monday": "Open 24 hours",
|
||||
"tuesday": "Open 24 hours",
|
||||
"wednesday": "Open 24 hours",
|
||||
"thursday": "Open 24 hours",
|
||||
"friday": "Open 24 hours"
|
||||
},
|
||||
"category": "Air conditioning contractor",
|
||||
"price_level": null,
|
||||
"photos_count": null,
|
||||
"description": null,
|
||||
"coordinates": {
|
||||
"lat": 38.5640548,
|
||||
"lon": -121.3131153
|
||||
},
|
||||
"closed_status": {
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"sources": {
|
||||
"google_business_profile": {
|
||||
"name": "AirTech Pros",
|
||||
"address": "10210 Systems Pkwy #370, Sacramento, CA 95827",
|
||||
"phone": "(530) 303-2225",
|
||||
"website": "https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"rating": 4.8,
|
||||
"reviews": 1121,
|
||||
"hours": {
|
||||
"saturday": "Open 24 hours",
|
||||
"sunday": "Open 24 hours",
|
||||
"monday": "Open 24 hours",
|
||||
"tuesday": "Open 24 hours",
|
||||
"wednesday": "Open 24 hours",
|
||||
"thursday": "Open 24 hours",
|
||||
"friday": "Open 24 hours"
|
||||
},
|
||||
"category": "Air conditioning contractor",
|
||||
"price_level": null,
|
||||
"description": null,
|
||||
"photos_count": null,
|
||||
"latitude": 38.5640548,
|
||||
"longitude": -121.3131153,
|
||||
"url": "https://www.google.com/maps/place/AirTech+Pros/@38.5640548,-121.3131153,17z/data=!3m1!4b1!4m6!3m5!1s0x809a50bf285b81c3:0xaf0247bad6e5ba5!8m2!3d38.5640548!4d-121.3131153!16s%2Fg%2F1260vg0nw?entry=ttu&g_ep=EgoyMDI2MDgxMi4wIKXMDSoASAFQAw%3D%3D",
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"apple_maps": {
|
||||
"address": "4961 Windplay Dr, El Dorado Hills, CA 95762",
|
||||
"hours": {
|
||||
"sunday": "12:00 AM-12:00 PM",
|
||||
"monday": "12:00 AM-12:00 PM",
|
||||
"tuesday": "12:00 AM-12:00 PM",
|
||||
"wednesday": "12:00 AM-12:00 PM",
|
||||
"thursday": "12:00 AM-12:00 PM",
|
||||
"friday": "12:00 AM-12:00 PM",
|
||||
"saturday": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"coordinates": {
|
||||
"lat": 38.638753,
|
||||
"lon": -121.071981
|
||||
},
|
||||
"name": "Air Tech HVAC",
|
||||
"category": "Consumer Sector",
|
||||
"phone": "+1 (916) 930-0010",
|
||||
"website": "http://teamairtech.com/"
|
||||
},
|
||||
"bing_places": null,
|
||||
"website": {
|
||||
"source_url": "https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"title": "AC Repair & Heating Services in Sacramento, CA | Air Tech Pros",
|
||||
"description": "Trusted AC repair and heating experts serving Sacramento and Cameron Park, CA. Get professional HVAC service, repairs, replacements, and maintenance.",
|
||||
"phone_on_page": "0000000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"audit_engine": "v1",
|
||||
"timestamp": "2026-08-15T13:54:36.820128+00:00",
|
||||
"business": "AirTech Pros",
|
||||
"summary": {
|
||||
"total_findings": 8,
|
||||
"severity_counts": {
|
||||
"immediate": 3,
|
||||
"high": 1,
|
||||
"medium": 3,
|
||||
"enhancement": 1
|
||||
},
|
||||
"sub_score_issues": {
|
||||
"Digital Identity": 4,
|
||||
"Google Business": 1,
|
||||
"Reviews": 1,
|
||||
"Technical SEO": 1,
|
||||
"Content": 1
|
||||
},
|
||||
"surfaces_ok": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"website": true
|
||||
},
|
||||
"surfaces_failed": {
|
||||
"bing_places": false
|
||||
},
|
||||
"rating": 4.8,
|
||||
"reviews": 1121
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "nap_phone_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Phone number inconsistent across 2 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "(530) 303-2225",
|
||||
"apple": "+1 (916) 930-0010",
|
||||
"website": "0000000000"
|
||||
},
|
||||
"recommendation": "Standardize phone format across all directories. GBP is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "nap_address_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Address inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "10210 Systems Pkwy #370, Sacramento, CA 95827",
|
||||
"apple": "4961 Windplay Dr, El Dorado Hills, CA 95762"
|
||||
},
|
||||
"recommendation": "Standardize exact address format. GBP address is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "hours_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Hours mismatch on 7 day(s) \u2014 GBP is authoritative",
|
||||
"sub_score": "Google Business",
|
||||
"evidence": [
|
||||
{
|
||||
"day": "saturday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "sunday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
}
|
||||
],
|
||||
"recommendation": "Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."
|
||||
},
|
||||
{
|
||||
"id": "nap_website_mismatch",
|
||||
"severity": "high",
|
||||
"title": "Website URL inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"apple": "http://teamairtech.com/"
|
||||
},
|
||||
"recommendation": "Use one canonical URL (prefer HTTPS, no www) across all directories."
|
||||
},
|
||||
{
|
||||
"id": "review_dates_unavailable",
|
||||
"severity": "medium",
|
||||
"title": "Cannot determine review recency \u2014 no dates available in samples",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sources_with_dates": []
|
||||
},
|
||||
"recommendation": "Review date data is not available from current surfaces. Monitor via GBP dashboard directly."
|
||||
},
|
||||
{
|
||||
"id": "category_fragmentation",
|
||||
"severity": "medium",
|
||||
"title": "Category varies across surfaces: air conditioning contractor, consumer sector",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "air conditioning contractor",
|
||||
"apple": "consumer sector"
|
||||
},
|
||||
"recommendation": "Align primary category across all directories. GBP primary category is the ranking driver."
|
||||
},
|
||||
{
|
||||
"id": "schema_wrong_type",
|
||||
"severity": "medium",
|
||||
"title": "JSON-LD present but no LocalBusiness/BeautySalon type",
|
||||
"sub_score": "Technical SEO",
|
||||
"evidence": {
|
||||
"url": "https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"has_jsonld": true,
|
||||
"has_local_business": false
|
||||
},
|
||||
"recommendation": "Ensure schema includes LocalBusiness or BeautySalon @type for local search relevance."
|
||||
},
|
||||
{
|
||||
"id": "no_price_level",
|
||||
"severity": "enhancement",
|
||||
"title": "Price level not set on GBP or Apple Maps",
|
||||
"sub_score": "Content",
|
||||
"evidence": {},
|
||||
"recommendation": "Set price level ($\u2013$$$$) on GBP. Helps users self-select and improves relevance."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
# Audit Findings — AirTech Pros
|
||||
|
||||
**Generated:** 2026-08-15 13:54 UTC
|
||||
**Sources checked:** google_business_profile, apple_maps, website
|
||||
**Rating:** 4.8 | **Reviews:** 1121
|
||||
|
||||
## Severity Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🟥 Immediate | 3 |
|
||||
| 🟧 High | 1 |
|
||||
| 🟨 Medium | 3 |
|
||||
| 🟩 Enhancement | 1 |
|
||||
| **Total** | **8** |
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding #1: Phone number inconsistent across 2 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize phone format across all directories. GBP is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "(530) 303-2225",
|
||||
"apple": "+1 (916) 930-0010",
|
||||
"website": "0000000000"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #2: Address inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize exact address format. GBP address is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "10210 Systems Pkwy #370, Sacramento, CA 95827",
|
||||
"apple": "4961 Windplay Dr, El Dorado Hills, CA 95762"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #3: Hours mismatch on 7 day(s) — GBP is authoritative
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Google Business
|
||||
- **Recommendation:** Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss.
|
||||
- **Evidence:** ```json
|
||||
[
|
||||
{
|
||||
"day": "saturday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "sunday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Finding #4: Website URL inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Use one canonical URL (prefer HTTPS, no www) across all directories.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"apple": "http://teamairtech.com/"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #5: Cannot determine review recency — no dates available in samples
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Review date data is not available from current surfaces. Monitor via GBP dashboard directly.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sources_with_dates": []
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #6: Category varies across surfaces: air conditioning contractor, consumer sector
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Align primary category across all directories. GBP primary category is the ranking driver.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "air conditioning contractor",
|
||||
"apple": "consumer sector"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #7: JSON-LD present but no LocalBusiness/BeautySalon type
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Technical SEO
|
||||
- **Recommendation:** Ensure schema includes LocalBusiness or BeautySalon @type for local search relevance.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"url": "https://airtechpros.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"has_jsonld": true,
|
||||
"has_local_business": false
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #8: Price level not set on GBP or Apple Maps
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Set price level ($–$$$$) on GBP. Helps users self-select and improves relevance.
|
||||
|
||||
## Failed Surfaces
|
||||
|
||||
- ❌ `bing_places` — no data returned
|
||||
@@ -0,0 +1,73 @@
|
||||
# Cameron Park Family Dentistry
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** Cameron Park, CA
|
||||
**Vertical:** Dental
|
||||
**Audit date:** 15 August 2026
|
||||
**Prepared for:** Cameron Park Family Dentistry
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited Cameron Park Family Dentistry across its primary online surfaces: Google Business Profile, Apple Maps and Website. The business shows 4.9 ★ across 133 Google reviews.
|
||||
**2 material integrity issues were identified:**
|
||||
|
||||
1. Phone numbers are inconsistent across directories.
|
||||
2. The website lacks LocalBusiness structured data.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✓ | 4.9 ★ | 133 |
|
||||
| Apple Maps | ✓ | 5 ★ | 13 |
|
||||
| Bing Places | ✗ | capture blocked (bot-wall) — excluded, not verified | — |
|
||||
| Website | ✓ | — | — |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Phone number inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists **(530) 677-0272**. Apple lists **+1 (530) 677-0272**. Website lists **6560355451**.
|
||||
|
||||
**Impact:**
|
||||
Customers reaching the business through a single directory may call a disconnected or wrong number. Inconsistent NAP (name-address-phone) signals reduce local search ranking across all surfaces.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the correct primary number, then update every directory to match. Keep the verified number identical across Google, Apple, and the website.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: JSON-LD present but no LocalBusiness type
|
||||
|
||||
**Observation:**
|
||||
schema at https://cameronparkfamilydentistry.com/ lacks LocalBusiness/Dentist/Plumber type — AI/structured data can't classify the business
|
||||
|
||||
**Impact:**
|
||||
Without LocalBusiness structured data, AI assistants and structured search cannot reliably classify or cite the business's services, hours, and area served.
|
||||
|
||||
**Recommendation:**
|
||||
Add LocalBusiness JSON-LD (Dentist/Plumber/Contractor type) with name, address, phone, hours, and areaServed.
|
||||
|
||||
---
|
||||
|
||||
## Verification Pending
|
||||
|
||||
- **Hours mismatch on 5 day(s) — GBP is authoritative** — 1 day(s) affected by '1800' parse artifact: friday: 8 AM-3 PM vs 8:1800 AM-3:00 PM — verify against live Apple Maps; excluded from report until confirmed
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; ratings and counts only.
|
||||
- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*
|
||||
@@ -0,0 +1,49 @@
|
||||
# Validation Run: Cameron Park Family Dentistry
|
||||
|
||||
**Date:** 15 August 2026
|
||||
**Business:** Cameron Park Family Dentistry
|
||||
**Rating:** 4.9 ★ | **Reviews:** 133
|
||||
**Engine findings:** 9 | **Material (in report):** 2 | **Rejected FP/low-signal:** 4 | **Needs verification:** 1
|
||||
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
### Material (survive normalization → in REPORT-final.md)
|
||||
|
||||
- **immediate** — Phone number inconsistent across 1 surface(s)
|
||||
`distinct numbers: 5306770272, 6560355451`
|
||||
- **medium** — JSON-LD present but no LocalBusiness type
|
||||
`schema at https://cameronparkfamilydentistry.com/ lacks LocalBusiness/Dentist/Plumber type — AI/structured data can't classify the business`
|
||||
|
||||
### Needs verification (excluded from report until confirmed)
|
||||
|
||||
- **immediate** — Hours mismatch on 5 day(s) — GBP is authoritative
|
||||
`1 day(s) affected by '1800' parse artifact: friday: 8 AM-3 PM vs 8:1800 AM-3:00 PM — verify against live Apple Maps; excluded from report until confirmed`
|
||||
|
||||
### Rejected (format false-positives / low-signal / tooling gaps)
|
||||
|
||||
- ~~Address inconsistent across 1 surface(s)~~ — same street/city/zip; suite format only (# vs Ste vs Unit)
|
||||
- ~~Review count varies by 120 across surfaces~~ — low-signal: platforms count reviews differently; not an integrity issue
|
||||
- ~~Cannot determine review recency — no dates available in samples~~ — tooling gap: scraper captures no review dates (documented limitation)
|
||||
- ~~Category varies across surfaces: dentist, health care~~ — low-signal: per-platform taxonomy (Apple 'consumer sector' is its default)
|
||||
|
||||
### Enhancement tier (noted, excluded from client report)
|
||||
|
||||
- Only 0% of sample reviews mention specific services or practitioners
|
||||
- Price level not set on GBP or Apple Maps
|
||||
|
||||
## Pre-delivery consistency gate (v1.1 §6)
|
||||
- Count match: executive summary states 2; Findings section contains 2 ✅
|
||||
- Evidence present: every cited value exists in findings.json / raw capture ✅
|
||||
- Identity match: name, location, and date match intake record ✅
|
||||
|
||||
*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*
|
||||
|
||||
## GATE RESULT
|
||||
|
||||
**PASS** — report_gate.py, 2026-08-15T16:27:11
|
||||
|
||||
- Count: 2 stated / 2 material after review — match
|
||||
- Identity: report title + date match raw capture ('Cameron Park Family Dentistry', 15 August 2026) — match
|
||||
- Surface disclosure: all captured/failed surfaces rendered correctly — match
|
||||
- Material support: every material finding re-derived from raw capture — hold
|
||||
- Needs-verification open: 1
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"audit": {
|
||||
"timestamp": "2026-08-15T13:51:27.900507+00:00",
|
||||
"tool": "multi_scraper_v1",
|
||||
"surfaces_checked": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": false,
|
||||
"website": true
|
||||
},
|
||||
"verification": {
|
||||
"phone_mismatch": {
|
||||
"primary": "(530) 677-0272",
|
||||
"conflicts": {
|
||||
"apple": "+1 (530) 677-0272"
|
||||
}
|
||||
},
|
||||
"hours_mismatch": {
|
||||
"monday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"tuesday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"wednesday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"thursday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"friday": {
|
||||
"google": "8 AM-3 PM",
|
||||
"apple": "8:1800 AM-3:00 PM"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"primary_source": "google_business_profile",
|
||||
"name": "Cameron Park Family Dentistry",
|
||||
"address": "4058 Flying C Rd #9, Cameron Park, CA 95682",
|
||||
"phone": "(530) 677-0272",
|
||||
"website": "https://cameronparkfamilydentistry.com/",
|
||||
"rating": 4.9,
|
||||
"reviews": 133,
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Called Cameron Park Family Dentistry when a crown I had received 6 weeks prior from a different dentist had abscessed . I was stressed and dealing with a series of other personal things and feeling ov",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "Had a dental emergency and Dr. Nav and his office was able to get me in same day! They were even accommodating with my busy schedule of work and school. Everyone is always very friendly and welcoming",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "I have had very bad experiences from other dentists and their offices, BUT this office with Dr. Nav and Cassie (?) I have no complaints. The front office works great and is a nice flow. The back offic",
|
||||
"rating": 5
|
||||
}
|
||||
],
|
||||
"hours": {
|
||||
"saturday": "Closed",
|
||||
"sunday": "Closed",
|
||||
"monday": "8 AM-5 PM",
|
||||
"tuesday": "8 AM-5 PM",
|
||||
"wednesday": "8 AM-5 PM",
|
||||
"thursday": "8 AM-5 PM",
|
||||
"friday": "8 AM-3 PM"
|
||||
},
|
||||
"category": "Dentist",
|
||||
"price_level": null,
|
||||
"photos_count": null,
|
||||
"description": null,
|
||||
"coordinates": {
|
||||
"lat": 38.6553486,
|
||||
"lon": -120.9971765
|
||||
},
|
||||
"closed_status": {
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"sources": {
|
||||
"google_business_profile": {
|
||||
"name": "Cameron Park Family Dentistry",
|
||||
"address": "4058 Flying C Rd #9, Cameron Park, CA 95682",
|
||||
"phone": "(530) 677-0272",
|
||||
"website": "https://cameronparkfamilydentistry.com/",
|
||||
"rating": 4.9,
|
||||
"reviews": 133,
|
||||
"hours": {
|
||||
"saturday": "Closed",
|
||||
"sunday": "Closed",
|
||||
"monday": "8 AM-5 PM",
|
||||
"tuesday": "8 AM-5 PM",
|
||||
"wednesday": "8 AM-5 PM",
|
||||
"thursday": "8 AM-5 PM",
|
||||
"friday": "8 AM-3 PM"
|
||||
},
|
||||
"category": "Dentist",
|
||||
"price_level": null,
|
||||
"description": null,
|
||||
"photos_count": null,
|
||||
"latitude": 38.6553486,
|
||||
"longitude": -120.9971765,
|
||||
"url": "https://www.google.com/maps/place/Cameron+Park+Family+Dentistry/@38.6553486,-120.9971765,17z/data=!3m1!4b1!4m6!3m5!1s0x809af0a83001e2af:0x726e3d79aa2529a8!8m2!3d38.6553486!4d-120.9971765!16s%2Fg%2F1wnf1973?entry=ttu&g_ep=EgoyMDI2MDgxMi4wIKXMDSoASAFQAw%3D%3D",
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"apple_maps": {
|
||||
"address": "4058 Flying C Rd, Ste 9, Cameron Park, CA 95682",
|
||||
"hours": {
|
||||
"monday": "8:00 AM-5:00 PM",
|
||||
"tuesday": "8:00 AM-5:00 PM",
|
||||
"wednesday": "8:00 AM-5:00 PM",
|
||||
"thursday": "8:00 AM-5:00 PM",
|
||||
"friday": "8:1800 AM-3:00 PM"
|
||||
},
|
||||
"rating": 5,
|
||||
"reviews": 13,
|
||||
"coordinates": {
|
||||
"lat": 38.6554799,
|
||||
"lon": -120.9969816
|
||||
},
|
||||
"name": "Cameron Park Family Dentistry",
|
||||
"category": "Health Care",
|
||||
"phone": "+1 (530) 677-0272",
|
||||
"website": "https://cameronparkfamilydentistry.com/",
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Called Cameron Park Family Dentistry when a crown I had received 6 weeks prior from a different dentist had abscessed . I was stressed and dealing with a series of other personal things and feeling ov",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "Had a dental emergency and Dr. Nav and his office was able to get me in same day! They were even accommodating with my busy schedule of work and school. Everyone is always very friendly and welcoming",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "I have had very bad experiences from other dentists and their offices, BUT this office with Dr. Nav and Cassie (?) I have no complaints. The front office works great and is a nice flow. The back offic",
|
||||
"rating": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"bing_places": null,
|
||||
"website": {
|
||||
"source_url": "https://cameronparkfamilydentistry.com/",
|
||||
"title": "Family Dentistry in Cameron Park, CA | Top-Rated Dentist",
|
||||
"description": "Looking for a top-rated dentist in Cameron Park? Dr. Nav & the team prioritize your comfort & deliver exceptional dental care for all ages.",
|
||||
"phone_on_page": "6560355451"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"audit_engine": "v1",
|
||||
"timestamp": "2026-08-15T13:51:28.438358+00:00",
|
||||
"business": "Cameron Park Family Dentistry",
|
||||
"summary": {
|
||||
"total_findings": 9,
|
||||
"severity_counts": {
|
||||
"immediate": 3,
|
||||
"high": 0,
|
||||
"medium": 4,
|
||||
"enhancement": 2
|
||||
},
|
||||
"sub_score_issues": {
|
||||
"Digital Identity": 3,
|
||||
"Google Business": 1,
|
||||
"Reviews": 3,
|
||||
"Technical SEO": 1,
|
||||
"Content": 1
|
||||
},
|
||||
"surfaces_ok": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"website": true
|
||||
},
|
||||
"surfaces_failed": {
|
||||
"bing_places": false
|
||||
},
|
||||
"rating": 4.9,
|
||||
"reviews": 133
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "nap_phone_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Phone number inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "(530) 677-0272",
|
||||
"apple": "+1 (530) 677-0272",
|
||||
"website": "6560355451"
|
||||
},
|
||||
"recommendation": "Standardize phone format across all directories. GBP is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "nap_address_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Address inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "4058 Flying C Rd #9, Cameron Park, CA 95682",
|
||||
"apple": "4058 Flying C Rd, Ste 9, Cameron Park, CA 95682"
|
||||
},
|
||||
"recommendation": "Standardize exact address format. GBP address is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "hours_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Hours mismatch on 5 day(s) \u2014 GBP is authoritative",
|
||||
"sub_score": "Google Business",
|
||||
"evidence": [
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "8 AM-3 PM",
|
||||
"apple_maps": "8:1800 AM-3:00 PM"
|
||||
}
|
||||
],
|
||||
"recommendation": "Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."
|
||||
},
|
||||
{
|
||||
"id": "review_count_delta",
|
||||
"severity": "medium",
|
||||
"title": "Review count varies by 120 across surfaces",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"google": 133,
|
||||
"apple": 13
|
||||
},
|
||||
"recommendation": "Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership."
|
||||
},
|
||||
{
|
||||
"id": "review_dates_unavailable",
|
||||
"severity": "medium",
|
||||
"title": "Cannot determine review recency \u2014 no dates available in samples",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sources_with_dates": []
|
||||
},
|
||||
"recommendation": "Review date data is not available from current surfaces. Monitor via GBP dashboard directly."
|
||||
},
|
||||
{
|
||||
"id": "category_fragmentation",
|
||||
"severity": "medium",
|
||||
"title": "Category varies across surfaces: dentist, health care",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "dentist",
|
||||
"apple": "health care"
|
||||
},
|
||||
"recommendation": "Align primary category across all directories. GBP primary category is the ranking driver."
|
||||
},
|
||||
{
|
||||
"id": "schema_wrong_type",
|
||||
"severity": "medium",
|
||||
"title": "JSON-LD present but no LocalBusiness/BeautySalon type",
|
||||
"sub_score": "Technical SEO",
|
||||
"evidence": {
|
||||
"url": "https://cameronparkfamilydentistry.com/",
|
||||
"has_jsonld": true,
|
||||
"has_local_business": false
|
||||
},
|
||||
"recommendation": "Ensure schema includes LocalBusiness or BeautySalon @type for local search relevance."
|
||||
},
|
||||
{
|
||||
"id": "reviews_low_specificity",
|
||||
"severity": "enhancement",
|
||||
"title": "Only 0% of sample reviews mention specific services or practitioners",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
},
|
||||
"recommendation": "Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better."
|
||||
},
|
||||
{
|
||||
"id": "no_price_level",
|
||||
"severity": "enhancement",
|
||||
"title": "Price level not set on GBP or Apple Maps",
|
||||
"sub_score": "Content",
|
||||
"evidence": {},
|
||||
"recommendation": "Set price level ($\u2013$$$$) on GBP. Helps users self-select and improves relevance."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
# Audit Findings — Cameron Park Family Dentistry
|
||||
|
||||
**Generated:** 2026-08-15 13:51 UTC
|
||||
**Sources checked:** google_business_profile, apple_maps, website
|
||||
**Rating:** 4.9 | **Reviews:** 133
|
||||
|
||||
## Severity Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🟥 Immediate | 3 |
|
||||
| 🟧 High | 0 |
|
||||
| 🟨 Medium | 4 |
|
||||
| 🟩 Enhancement | 2 |
|
||||
| **Total** | **9** |
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding #1: Phone number inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize phone format across all directories. GBP is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "(530) 677-0272",
|
||||
"apple": "+1 (530) 677-0272",
|
||||
"website": "6560355451"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #2: Address inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize exact address format. GBP address is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "4058 Flying C Rd #9, Cameron Park, CA 95682",
|
||||
"apple": "4058 Flying C Rd, Ste 9, Cameron Park, CA 95682"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #3: Hours mismatch on 5 day(s) — GBP is authoritative
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Google Business
|
||||
- **Recommendation:** Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss.
|
||||
- **Evidence:** ```json
|
||||
[
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "8 AM-3 PM",
|
||||
"apple_maps": "8:1800 AM-3:00 PM"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Finding #4: Review count varies by 120 across surfaces
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": 133,
|
||||
"apple": 13
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #5: Cannot determine review recency — no dates available in samples
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Review date data is not available from current surfaces. Monitor via GBP dashboard directly.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sources_with_dates": []
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #6: Category varies across surfaces: dentist, health care
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Align primary category across all directories. GBP primary category is the ranking driver.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "dentist",
|
||||
"apple": "health care"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #7: JSON-LD present but no LocalBusiness/BeautySalon type
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Technical SEO
|
||||
- **Recommendation:** Ensure schema includes LocalBusiness or BeautySalon @type for local search relevance.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"url": "https://cameronparkfamilydentistry.com/",
|
||||
"has_jsonld": true,
|
||||
"has_local_business": false
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #8: Only 0% of sample reviews mention specific services or practitioners
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #9: Price level not set on GBP or Apple Maps
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Set price level ($–$$$$) on GBP. Helps users self-select and improves relevance.
|
||||
|
||||
## Failed Surfaces
|
||||
|
||||
- ❌ `bing_places` — no data returned
|
||||
@@ -0,0 +1,69 @@
|
||||
# D. Martel Plumbing Service & Repair
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** El Dorado Hills, CA
|
||||
**Vertical:** Plumbing
|
||||
**Audit date:** 15 August 2026
|
||||
**Prepared for:** D. Martel Plumbing Service & Repair
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited D. Martel Plumbing Service & Repair across its primary online surfaces: Google Business Profile, Apple Maps, Bing Places and Website. The business shows 4.7 ★ across 104 Google reviews.
|
||||
**2 material integrity issues were identified:**
|
||||
|
||||
1. Business hours are inconsistent between Google and Apple.
|
||||
2. The website lacks LocalBusiness structured data.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✓ | 4.7 ★ | 104 |
|
||||
| Apple Maps | ✓ | 4.6 ★ | 251 |
|
||||
| Bing Places | ✓ | 4.5 ★ | 248 |
|
||||
| Website | ✓ | — | — |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Hours mismatch between Google and Apple
|
||||
|
||||
**Observation:**
|
||||
monday: Google “7 AM-7 PM” vs Apple “8:00 AM-5:00 PM”; tuesday: Google “7 AM-7 PM” vs Apple “8:00 AM-5:00 PM”; wednesday: Google “7 AM-7 PM” vs Apple “8:00 AM-5:00 PM”; thursday: Google “7 AM-7 PM” vs Apple “8:00 AM-5:00 PM”; friday: Google “7 AM-7 PM” vs Apple “8:00 AM-5:00 PM”
|
||||
|
||||
**Impact:**
|
||||
Customers planning a visit see different availability depending on which directory they use. Mismatched hours drive no-shows and lost calls at the edge of the workday.
|
||||
|
||||
**Recommendation:**
|
||||
Verify actual operating hours with the business. Update the non-authoritative surface to match Google's verified schedule, or correct Google if its hours are inaccurate.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: JSON-LD present but no LocalBusiness type
|
||||
|
||||
**Observation:**
|
||||
schema at https://dmartelplumbing.com/ lacks LocalBusiness/Dentist/Plumber type — AI/structured data can't classify the business
|
||||
|
||||
**Impact:**
|
||||
Without LocalBusiness structured data, AI assistants and structured search cannot reliably classify or cite the business's services, hours, and area served.
|
||||
|
||||
**Recommendation:**
|
||||
Add LocalBusiness JSON-LD (Dentist/Plumber/Contractor type) with name, address, phone, hours, and areaServed.
|
||||
|
||||
---
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; ratings and counts only.
|
||||
- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*
|
||||
@@ -0,0 +1,47 @@
|
||||
# Validation Run: D. Martel Plumbing Service & Repair
|
||||
|
||||
**Date:** 15 August 2026
|
||||
**Business:** D. Martel Plumbing Service & Repair
|
||||
**Rating:** 4.7 ★ | **Reviews:** 104
|
||||
**Engine findings:** 7 | **Material (in report):** 2 | **Rejected FP/low-signal:** 3 | **Needs verification:** 0
|
||||
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
### Material (survive normalization → in REPORT-final.md)
|
||||
|
||||
- **immediate** — Hours mismatch on 5 day(s) — GBP is authoritative
|
||||
`5 day(s) genuinely differ: monday: 7 AM-7 PM vs 8:00 AM-5:00 PM; tuesday: 7 AM-7 PM vs 8:00 AM-5:00 PM; wednesday: 7 AM-7 PM vs 8:00 AM-5:00 PM; thursday: 7 AM-7 PM vs 8:00 AM-5:00 PM`
|
||||
- **medium** — JSON-LD present but no LocalBusiness type
|
||||
`schema at https://dmartelplumbing.com/ lacks LocalBusiness/Dentist/Plumber type — AI/structured data can't classify the business`
|
||||
|
||||
### Needs verification (excluded from report until confirmed)
|
||||
|
||||
- none
|
||||
|
||||
### Rejected (format false-positives / low-signal / tooling gaps)
|
||||
|
||||
- ~~Address inconsistent across 1 surface(s)~~ — same street/city/zip; suite format only (# vs Ste vs Unit)
|
||||
- ~~Review count varies by 147 across surfaces~~ — low-signal: platforms count reviews differently; not an integrity issue
|
||||
- ~~Category varies across surfaces: consumer sector, home service, plumber~~ — low-signal: per-platform taxonomy (Apple 'consumer sector' is its default)
|
||||
|
||||
### Enhancement tier (noted, excluded from client report)
|
||||
|
||||
- Only 0% of sample reviews mention specific services or practitioners
|
||||
- Price level not set on GBP or Apple Maps
|
||||
|
||||
## Pre-delivery consistency gate (v1.1 §6)
|
||||
- Count match: executive summary states 2; Findings section contains 2 ✅
|
||||
- Evidence present: every cited value exists in findings.json / raw capture ✅
|
||||
- Identity match: name, location, and date match intake record ✅
|
||||
|
||||
*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*
|
||||
|
||||
## GATE RESULT
|
||||
|
||||
**PASS** — report_gate.py, 2026-08-15T16:27:11
|
||||
|
||||
- Count: 2 stated / 2 material after review — match
|
||||
- Identity: report title + date match raw capture ('D. Martel Plumbing Service & Repair', 15 August 2026) — match
|
||||
- Surface disclosure: all captured/failed surfaces rendered correctly — match
|
||||
- Material support: every material finding re-derived from raw capture — hold
|
||||
- Needs-verification open: 0
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"audit": {
|
||||
"timestamp": "2026-08-15T13:53:01.924693+00:00",
|
||||
"tool": "multi_scraper_v1",
|
||||
"surfaces_checked": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": true,
|
||||
"website": true
|
||||
},
|
||||
"verification": {
|
||||
"name_mismatch": {
|
||||
"primary": "D. Martel Plumbing Service & Repair",
|
||||
"conflicts": {
|
||||
"apple": "D Martel Plumbing"
|
||||
}
|
||||
},
|
||||
"phone_mismatch": {
|
||||
"primary": "(916) 933-6363",
|
||||
"conflicts": {
|
||||
"apple": "+1 (916) 933-6363"
|
||||
}
|
||||
},
|
||||
"website_mismatch": {
|
||||
"primary": "https://dmartelplumbing.com/",
|
||||
"conflicts": {
|
||||
"apple": "https://www.dmartelplumbing.com"
|
||||
}
|
||||
},
|
||||
"hours_mismatch": {
|
||||
"monday": {
|
||||
"google": "7 AM-7 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"tuesday": {
|
||||
"google": "7 AM-7 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"wednesday": {
|
||||
"google": "7 AM-7 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"thursday": {
|
||||
"google": "7 AM-7 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"friday": {
|
||||
"google": "7 AM-7 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"primary_source": "google_business_profile",
|
||||
"name": "D. Martel Plumbing Service & Repair",
|
||||
"address": "1132 Suncast Ln #2, El Dorado Hills, CA 95762",
|
||||
"phone": "(916) 933-6363",
|
||||
"website": "https://dmartelplumbing.com/",
|
||||
"rating": 4.7,
|
||||
"reviews": 104,
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Have used D Martel a few times and Zack is always prompt and professional. Work is completed with great results. Would recommend this business.",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "Our toilet had a leak that spiked our water bill through the roof. This company was very responsive and quickly scheduled a technician to come out. Zach came out and was super knowledgeable and explai",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "We have not needed much plumbing service and had our main water line feeding the house decided to give. I found D Martel on Nextdoor and like to use local contractors. They lived up to their rating, c",
|
||||
"rating": 5
|
||||
}
|
||||
],
|
||||
"hours": {
|
||||
"saturday": "7 AM-5 PM",
|
||||
"sunday": "Closed",
|
||||
"monday": "7 AM-7 PM",
|
||||
"tuesday": "7 AM-7 PM",
|
||||
"wednesday": "7 AM-7 PM",
|
||||
"thursday": "7 AM-7 PM",
|
||||
"friday": "7 AM-7 PM"
|
||||
},
|
||||
"category": "Plumber",
|
||||
"price_level": null,
|
||||
"photos_count": null,
|
||||
"description": null,
|
||||
"coordinates": {
|
||||
"lat": 38.6349036,
|
||||
"lon": -121.0655397
|
||||
},
|
||||
"closed_status": {
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"sources": {
|
||||
"google_business_profile": {
|
||||
"name": "D. Martel Plumbing Service & Repair",
|
||||
"address": "1132 Suncast Ln #2, El Dorado Hills, CA 95762",
|
||||
"phone": "(916) 933-6363",
|
||||
"website": "https://dmartelplumbing.com/",
|
||||
"rating": 4.7,
|
||||
"reviews": 104,
|
||||
"hours": {
|
||||
"saturday": "7 AM-5 PM",
|
||||
"sunday": "Closed",
|
||||
"monday": "7 AM-7 PM",
|
||||
"tuesday": "7 AM-7 PM",
|
||||
"wednesday": "7 AM-7 PM",
|
||||
"thursday": "7 AM-7 PM",
|
||||
"friday": "7 AM-7 PM"
|
||||
},
|
||||
"category": "Plumber",
|
||||
"price_level": null,
|
||||
"description": null,
|
||||
"photos_count": null,
|
||||
"latitude": 38.6349036,
|
||||
"longitude": -121.0655397,
|
||||
"url": "https://www.google.com/maps/place/D.+Martel+Plumbing+Service+%26+Repair/@38.6349036,-121.0655397,17z/data=!3m1!4b1!4m6!3m5!1s0x809af008333dcda9:0xbf2ea84c7004f241!8m2!3d38.6349036!4d-121.0655397!16s%2Fg%2F11cjkmlp0l?entry=ttu&g_ep=EgoyMDI2MDgxMi4wIKXMDSoASAFQAw%3D%3D",
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"apple_maps": {
|
||||
"address": "1132 Suncast Ln, Ste 2, El Dorado Hills, CA 95762",
|
||||
"hours": {
|
||||
"monday": "8:00 AM-5:00 PM",
|
||||
"tuesday": "8:00 AM-5:00 PM",
|
||||
"wednesday": "8:00 AM-5:00 PM",
|
||||
"thursday": "8:00 AM-5:00 PM",
|
||||
"friday": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"rating": 4.6,
|
||||
"reviews": 251,
|
||||
"coordinates": {
|
||||
"lat": 38.63503,
|
||||
"lon": -121.06557
|
||||
},
|
||||
"name": "D Martel Plumbing",
|
||||
"category": "Consumer Sector",
|
||||
"phone": "+1 (916) 933-6363",
|
||||
"website": "https://www.dmartelplumbing.com",
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Have used D Martel a few times and Zack is always prompt and professional. Work is completed with great results. Would recommend this business.",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "Our toilet had a leak that spiked our water bill through the roof. This company was very responsive and quickly scheduled a technician to come out. Zach came out and was super knowledgeable and explai",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "We have not needed much plumbing service and had our main water line feeding the house decided to give. I found D Martel on Nextdoor and like to use local contractors. They lived up to their rating, c",
|
||||
"rating": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"bing_places": {
|
||||
"name": "D. Martel Plumbing Service & Repair",
|
||||
"rating": 4.5,
|
||||
"reviews": 248,
|
||||
"category": "Home service",
|
||||
"phone": "(916) 933-6363",
|
||||
"hours": {
|
||||
"monday": "7 AM - 7 PM",
|
||||
"tuesday": "7 AM - 7 PM",
|
||||
"wednesday": "7 AM - 7 PM",
|
||||
"thursday": "7 AM - 7 PM",
|
||||
"friday": "7 AM - 7 PM",
|
||||
"saturday": "7 AM - 5 PM",
|
||||
"sunday": "Closed"
|
||||
},
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Our toilet had a leak that spiked our water bill through the roof. This company was very responsive and quickly scheduled a technician to come out. Zach came out and was super knowledgeable and expla…",
|
||||
"date": "Jun 29, 2026"
|
||||
},
|
||||
{
|
||||
"text": "We have not needed much plumbing service and had our main water line feeding the house decided to give. I found D Martel on Nextdoor and like to use local contractors. They lived up to their rating, …",
|
||||
"date": "Jun 23, 2026"
|
||||
},
|
||||
{
|
||||
"text": "I was very happy with D. Martel Plumbing. The plumber came a little early, telephoned first to ask if that was okay, did their work and it's really beautiful--copper pipes, beautiful soldering job on…",
|
||||
"date": "Jun 23, 2026"
|
||||
}
|
||||
]
|
||||
},
|
||||
"website": {
|
||||
"source_url": "https://dmartelplumbing.com/",
|
||||
"title": "D. Martel Plumbing | Residential Plumbing Repair & Installation",
|
||||
"description": "Call Today (916) 933-6363 | Your local plumbing experts since 1991. Water heaters, leaks, clogs, repairs, installs and more. Free Estimates",
|
||||
"phone_on_page": "(916) 933-6363"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"audit_engine": "v1",
|
||||
"timestamp": "2026-08-15T13:53:02.361145+00:00",
|
||||
"business": "D. Martel Plumbing Service & Repair",
|
||||
"summary": {
|
||||
"total_findings": 7,
|
||||
"severity_counts": {
|
||||
"immediate": 2,
|
||||
"high": 0,
|
||||
"medium": 3,
|
||||
"enhancement": 2
|
||||
},
|
||||
"sub_score_issues": {
|
||||
"Digital Identity": 2,
|
||||
"Google Business": 1,
|
||||
"Reviews": 2,
|
||||
"Technical SEO": 1,
|
||||
"Content": 1
|
||||
},
|
||||
"surfaces_ok": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": true,
|
||||
"website": true
|
||||
},
|
||||
"surfaces_failed": {},
|
||||
"rating": 4.7,
|
||||
"reviews": 104
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "nap_address_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Address inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "1132 Suncast Ln #2, El Dorado Hills, CA 95762",
|
||||
"apple": "1132 Suncast Ln, Ste 2, El Dorado Hills, CA 95762"
|
||||
},
|
||||
"recommendation": "Standardize exact address format. GBP address is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "hours_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Hours mismatch on 5 day(s) \u2014 GBP is authoritative",
|
||||
"sub_score": "Google Business",
|
||||
"evidence": [
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "7 AM-7 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "7 AM-7 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "7 AM-7 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "7 AM-7 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "7 AM-7 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
}
|
||||
],
|
||||
"recommendation": "Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."
|
||||
},
|
||||
{
|
||||
"id": "review_count_delta",
|
||||
"severity": "medium",
|
||||
"title": "Review count varies by 147 across surfaces",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"google": 104,
|
||||
"apple": 251,
|
||||
"bing": 248
|
||||
},
|
||||
"recommendation": "Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership."
|
||||
},
|
||||
{
|
||||
"id": "category_fragmentation",
|
||||
"severity": "medium",
|
||||
"title": "Category varies across surfaces: consumer sector, home service, plumber",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "plumber",
|
||||
"apple": "consumer sector",
|
||||
"bing": "home service"
|
||||
},
|
||||
"recommendation": "Align primary category across all directories. GBP primary category is the ranking driver."
|
||||
},
|
||||
{
|
||||
"id": "schema_wrong_type",
|
||||
"severity": "medium",
|
||||
"title": "JSON-LD present but no LocalBusiness/BeautySalon type",
|
||||
"sub_score": "Technical SEO",
|
||||
"evidence": {
|
||||
"url": "https://dmartelplumbing.com/",
|
||||
"has_jsonld": true,
|
||||
"has_local_business": false
|
||||
},
|
||||
"recommendation": "Ensure schema includes LocalBusiness or BeautySalon @type for local search relevance."
|
||||
},
|
||||
{
|
||||
"id": "reviews_low_specificity",
|
||||
"severity": "enhancement",
|
||||
"title": "Only 0% of sample reviews mention specific services or practitioners",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sample_size": 6,
|
||||
"specific_count": 0
|
||||
},
|
||||
"recommendation": "Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better."
|
||||
},
|
||||
{
|
||||
"id": "no_price_level",
|
||||
"severity": "enhancement",
|
||||
"title": "Price level not set on GBP or Apple Maps",
|
||||
"sub_score": "Content",
|
||||
"evidence": {},
|
||||
"recommendation": "Set price level ($\u2013$$$$) on GBP. Helps users self-select and improves relevance."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# Audit Findings — D. Martel Plumbing Service & Repair
|
||||
|
||||
**Generated:** 2026-08-15 13:53 UTC
|
||||
**Sources checked:** google_business_profile, apple_maps, bing_places, website
|
||||
**Rating:** 4.7 | **Reviews:** 104
|
||||
|
||||
## Severity Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🟥 Immediate | 2 |
|
||||
| 🟧 High | 0 |
|
||||
| 🟨 Medium | 3 |
|
||||
| 🟩 Enhancement | 2 |
|
||||
| **Total** | **7** |
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding #1: Address inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize exact address format. GBP address is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "1132 Suncast Ln #2, El Dorado Hills, CA 95762",
|
||||
"apple": "1132 Suncast Ln, Ste 2, El Dorado Hills, CA 95762"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #2: Hours mismatch on 5 day(s) — GBP is authoritative
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Google Business
|
||||
- **Recommendation:** Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss.
|
||||
- **Evidence:** ```json
|
||||
[
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "7 AM-7 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "7 AM-7 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "7 AM-7 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "7 AM-7 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "7 AM-7 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Finding #3: Review count varies by 147 across surfaces
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": 104,
|
||||
"apple": 251,
|
||||
"bing": 248
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #4: Category varies across surfaces: consumer sector, home service, plumber
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Align primary category across all directories. GBP primary category is the ranking driver.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "plumber",
|
||||
"apple": "consumer sector",
|
||||
"bing": "home service"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #5: JSON-LD present but no LocalBusiness/BeautySalon type
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Technical SEO
|
||||
- **Recommendation:** Ensure schema includes LocalBusiness or BeautySalon @type for local search relevance.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"url": "https://dmartelplumbing.com/",
|
||||
"has_jsonld": true,
|
||||
"has_local_business": false
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #6: Only 0% of sample reviews mention specific services or practitioners
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sample_size": 6,
|
||||
"specific_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #7: Price level not set on GBP or Apple Maps
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Set price level ($–$$$$) on GBP. Helps users self-select and improves relevance.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Diamond Springs Dental Center
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** El Dorado, CA
|
||||
**Vertical:** Dental
|
||||
**Audit date:** 15 August 2026
|
||||
**Prepared for:** Diamond Springs Dental Center
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited Diamond Springs Dental Center across its primary online surfaces: Google Business Profile, Apple Maps and Website. The business shows 3.4 ★ across 49 Google reviews.
|
||||
**4 material integrity issues were identified:**
|
||||
|
||||
1. Phone numbers are inconsistent across directories.
|
||||
2. Address details differ between Google and Apple.
|
||||
3. Average rating is below the 4.0 trust threshold.
|
||||
4. The website lacks LocalBusiness structured data.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✓ | 3.4 ★ | 49 |
|
||||
| Apple Maps | ✓ | 3.3 ★ | 28 |
|
||||
| Bing Places | ✗ | capture blocked (bot-wall) — excluded, not verified | — |
|
||||
| Website | ✓ | — | — |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Phone number inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists **(530) 291-1888**. Apple lists **+1 (530) 344-0290**. Website lists **551-0858478**.
|
||||
|
||||
**Impact:**
|
||||
Customers reaching the business through a single directory may call a disconnected or wrong number. Inconsistent NAP (name-address-phone) signals reduce local search ranking across all surfaces.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the correct primary number, then update every directory to match. Keep the verified number identical across Google, Apple, and the website.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: Address inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists “6400 Pleasant Valley Rd, El Dorado, CA 95623”. Apple Maps lists “493 Pleasant Valley Rd, Ste 110, Diamond Springs, CA 95619”.
|
||||
|
||||
**Impact:**
|
||||
Conflicting addresses split local search signals and can send customers or service vehicles to the wrong location. NAP consistency is a core local-ranking factor.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the canonical street address (including unit) with the business, then align Google and Apple to the same string.
|
||||
|
||||
---
|
||||
|
||||
### Finding 3: Average rating 3.3 — below 4.0 threshold
|
||||
|
||||
**Observation:**
|
||||
Blended average is **3.35** (Google 3.4, Apple 3.3) — below the 4.0 trust threshold.
|
||||
|
||||
**Impact:**
|
||||
A sub-4.0 average is below the trust threshold for most local verticals and suppresses click-through in the local pack regardless of review volume.
|
||||
|
||||
**Recommendation:**
|
||||
Review the lower-rated reviews for recurring service issues; respond publicly to negative reviews and prioritize the service gaps they describe.
|
||||
|
||||
---
|
||||
|
||||
### Finding 4: No JSON-LD schema found on website
|
||||
|
||||
**Observation:**
|
||||
{"url": "https://www.diamondspringsdental.com/", "has_jsonld": false}
|
||||
|
||||
**Impact:**
|
||||
Without LocalBusiness structured data, AI assistants and structured search cannot reliably classify or cite the business's services, hours, and area served.
|
||||
|
||||
**Recommendation:**
|
||||
Add LocalBusiness JSON-LD (Dentist/Plumber/Contractor type) with name, address, phone, hours, and areaServed.
|
||||
|
||||
---
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; ratings and counts only.
|
||||
- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*
|
||||
@@ -0,0 +1,53 @@
|
||||
# Validation Run: Diamond Springs Dental Center
|
||||
|
||||
**Date:** 15 August 2026
|
||||
**Business:** Diamond Springs Dental Center
|
||||
**Rating:** 3.4 ★ | **Reviews:** 49
|
||||
**Engine findings:** 11 | **Material (in report):** 4 | **Rejected FP/low-signal:** 5 | **Needs verification:** 0
|
||||
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
### Material (survive normalization → in REPORT-final.md)
|
||||
|
||||
- **immediate** — Phone number inconsistent across 2 surface(s)
|
||||
`distinct numbers: 5302911888, 5303440290, 5510858478`
|
||||
- **immediate** — Address inconsistent across 1 surface(s)
|
||||
`google='6400 Pleasant Valley Rd, El Dorado, CA 95623' vs apple='493 Pleasant Valley Rd, Ste 110, Diamond Springs, CA 95619'`
|
||||
- **immediate** — Average rating 3.3 — below 4.0 threshold
|
||||
`average 3.35 (google 3.4 / apple 3.3)`
|
||||
- **high** — No JSON-LD schema found on website
|
||||
`no structured data at https://www.diamondspringsdental.com/`
|
||||
|
||||
### Needs verification (excluded from report until confirmed)
|
||||
|
||||
- none
|
||||
|
||||
### Rejected (format false-positives / low-signal / tooling gaps)
|
||||
|
||||
- ~~Hours mismatch on 4 day(s) — GBP is authoritative~~ — format only: '8 AM' vs '8:00 AM' — identical after parse
|
||||
- ~~Website URL inconsistent across 1 surface(s)~~ — same domain; http/https or www only
|
||||
- ~~Review count varies by 21 across surfaces~~ — low-signal: platforms count reviews differently; not an integrity issue
|
||||
- ~~Cannot determine review recency — no dates available in samples~~ — tooling gap: scraper captures no review dates (documented limitation)
|
||||
- ~~Category varies across surfaces: dental clinic, health care~~ — low-signal: per-platform taxonomy (Apple 'consumer sector' is its default)
|
||||
|
||||
### Enhancement tier (noted, excluded from client report)
|
||||
|
||||
- Only 0% of sample reviews mention specific services or practitioners
|
||||
- Price level not set on GBP or Apple Maps
|
||||
|
||||
## Pre-delivery consistency gate (v1.1 §6)
|
||||
- Count match: executive summary states 4; Findings section contains 4 ✅
|
||||
- Evidence present: every cited value exists in findings.json / raw capture ✅
|
||||
- Identity match: name, location, and date match intake record ✅
|
||||
|
||||
*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*
|
||||
|
||||
## GATE RESULT
|
||||
|
||||
**PASS** — report_gate.py, 2026-08-15T16:27:11
|
||||
|
||||
- Count: 4 stated / 4 material after review — match
|
||||
- Identity: report title + date match raw capture ('Diamond Springs Dental Center', 15 August 2026) — match
|
||||
- Surface disclosure: all captured/failed surfaces rendered correctly — match
|
||||
- Material support: every material finding re-derived from raw capture — hold
|
||||
- Needs-verification open: 0
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"audit": {
|
||||
"timestamp": "2026-08-15T13:56:16.107028+00:00",
|
||||
"tool": "multi_scraper_v1",
|
||||
"surfaces_checked": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": false,
|
||||
"website": true
|
||||
},
|
||||
"verification": {
|
||||
"phone_mismatch": {
|
||||
"primary": "(530) 291-1888",
|
||||
"conflicts": {
|
||||
"apple": "+1 (530) 344-0290"
|
||||
}
|
||||
},
|
||||
"website_mismatch": {
|
||||
"primary": "https://www.diamondspringsdental.com/",
|
||||
"conflicts": {
|
||||
"apple": "http://www.diamondspringsdental.com"
|
||||
}
|
||||
},
|
||||
"hours_mismatch": {
|
||||
"monday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"tuesday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"wednesday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"thursday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"primary_source": "google_business_profile",
|
||||
"name": "Diamond Springs Dental Center",
|
||||
"address": "6400 Pleasant Valley Rd, El Dorado, CA 95623",
|
||||
"phone": "(530) 291-1888",
|
||||
"website": "https://www.diamondspringsdental.com/",
|
||||
"rating": 3.4,
|
||||
"reviews": 49,
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Dr. Goodis, Dr. B, and the entire staff were amazing! I am a diabetic; just prior to my procedure, my sugar began to drop, Dr. Goodis gave me Capri Sun from his personal \"stash\". I would definitely re",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "DDS Goodis is the best Dentist I've ever had. He can pull a tooth unbelievably fast and painlessly. He's very conscientious and really cares about his patients teeth. I highly recommend him. His staff",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "Everybody there was so nice! Had a tooth pulled and it was so fast and pleasant, considering. Thank you!",
|
||||
"rating": 5
|
||||
}
|
||||
],
|
||||
"hours": {
|
||||
"saturday": "Closed",
|
||||
"sunday": "Closed",
|
||||
"monday": "8 AM-5 PM",
|
||||
"tuesday": "8 AM-5 PM",
|
||||
"wednesday": "8 AM-5 PM",
|
||||
"thursday": "8 AM-5 PM",
|
||||
"friday": "Closed"
|
||||
},
|
||||
"category": "Dental clinic",
|
||||
"price_level": null,
|
||||
"photos_count": null,
|
||||
"description": null,
|
||||
"coordinates": {
|
||||
"lat": 38.682032,
|
||||
"lon": -120.8397326
|
||||
},
|
||||
"closed_status": {
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"sources": {
|
||||
"google_business_profile": {
|
||||
"name": "Diamond Springs Dental Center",
|
||||
"address": "6400 Pleasant Valley Rd, El Dorado, CA 95623",
|
||||
"phone": "(530) 291-1888",
|
||||
"website": "https://www.diamondspringsdental.com/",
|
||||
"rating": 3.4,
|
||||
"reviews": 49,
|
||||
"hours": {
|
||||
"saturday": "Closed",
|
||||
"sunday": "Closed",
|
||||
"monday": "8 AM-5 PM",
|
||||
"tuesday": "8 AM-5 PM",
|
||||
"wednesday": "8 AM-5 PM",
|
||||
"thursday": "8 AM-5 PM",
|
||||
"friday": "Closed"
|
||||
},
|
||||
"category": "Dental clinic",
|
||||
"price_level": null,
|
||||
"description": null,
|
||||
"photos_count": null,
|
||||
"latitude": 38.682032,
|
||||
"longitude": -120.8397326,
|
||||
"url": "https://www.google.com/maps/place/Diamond+Springs+Dental+Center/@38.682032,-120.8397326,17z/data=!3m1!4b1!4m6!3m5!1s0x809a59edeffd7a51:0xf2f703daee67741b!8m2!3d38.682032!4d-120.8397326!16s%2Fg%2F1tfpk1qb?entry=ttu&g_ep=EgoyMDI2MDgxMi4wIKXMDSoASAFQAw%3D%3D",
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"apple_maps": {
|
||||
"address": "493 Pleasant Valley Rd, Ste 110, Diamond Springs, CA 95619",
|
||||
"hours": {
|
||||
"monday": "8:00 AM-5:00 PM",
|
||||
"tuesday": "8:00 AM-5:00 PM",
|
||||
"wednesday": "8:00 AM-5:00 PM",
|
||||
"thursday": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"rating": 3.3,
|
||||
"reviews": 28,
|
||||
"coordinates": {
|
||||
"lat": 38.6941234,
|
||||
"lon": -120.8178375
|
||||
},
|
||||
"name": "Diamond Springs Dental Center",
|
||||
"category": "Health Care",
|
||||
"phone": "+1 (530) 344-0290",
|
||||
"website": "http://www.diamondspringsdental.com",
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Dr. Goodis, Dr. B, and the entire staff were amazing! I am a diabetic; just prior to my procedure, my sugar began to drop, Dr. Goodis gave me Capri Sun from his personal \"stash\". I would definitely re",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "DDS Goodis is the best Dentist I've ever had. He can pull a tooth unbelievably fast and painlessly. He's very conscientious and really cares about his patients teeth. I highly recommend him. His staff",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "Everybody there was so nice! Had a tooth pulled and it was so fast and pleasant, considering. Thank you!",
|
||||
"rating": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"bing_places": null,
|
||||
"website": {
|
||||
"source_url": "https://www.diamondspringsdental.com/",
|
||||
"title": "Family Dentist in El Dorado, CA | Medi-Cal Dentist Near El Dorado Hills",
|
||||
"description": "Looking for a Medi-Cal dentist in El Dorado? Diamond Springs Dental Center offers gentle, advanced dental care for families. Serving El Dorado Hills & Placerville.",
|
||||
"phone_on_page": "551-0858478"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"audit_engine": "v1",
|
||||
"timestamp": "2026-08-15T13:56:16.462221+00:00",
|
||||
"business": "Diamond Springs Dental Center",
|
||||
"summary": {
|
||||
"total_findings": 11,
|
||||
"severity_counts": {
|
||||
"immediate": 4,
|
||||
"high": 2,
|
||||
"medium": 3,
|
||||
"enhancement": 2
|
||||
},
|
||||
"sub_score_issues": {
|
||||
"Digital Identity": 4,
|
||||
"Google Business": 1,
|
||||
"Reviews": 4,
|
||||
"Technical SEO": 1,
|
||||
"Content": 1
|
||||
},
|
||||
"surfaces_ok": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"website": true
|
||||
},
|
||||
"surfaces_failed": {
|
||||
"bing_places": false
|
||||
},
|
||||
"rating": 3.4,
|
||||
"reviews": 49
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "nap_phone_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Phone number inconsistent across 2 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "(530) 291-1888",
|
||||
"apple": "+1 (530) 344-0290",
|
||||
"website": "551-0858478"
|
||||
},
|
||||
"recommendation": "Standardize phone format across all directories. GBP is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "nap_address_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Address inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "6400 Pleasant Valley Rd, El Dorado, CA 95623",
|
||||
"apple": "493 Pleasant Valley Rd, Ste 110, Diamond Springs, CA 95619"
|
||||
},
|
||||
"recommendation": "Standardize exact address format. GBP address is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "hours_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Hours mismatch on 4 day(s) \u2014 GBP is authoritative",
|
||||
"sub_score": "Google Business",
|
||||
"evidence": [
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
}
|
||||
],
|
||||
"recommendation": "Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."
|
||||
},
|
||||
{
|
||||
"id": "rating_below_threshold",
|
||||
"severity": "immediate",
|
||||
"title": "Average rating 3.3 \u2014 below 4.0 threshold",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"average": 3.35,
|
||||
"by_source": {
|
||||
"google": 3.4,
|
||||
"apple": 3.3
|
||||
}
|
||||
},
|
||||
"recommendation": "Address negative reviews, request new reviews from satisfied customers. Below 4.0 is a strong conversion blocker."
|
||||
},
|
||||
{
|
||||
"id": "nap_website_mismatch",
|
||||
"severity": "high",
|
||||
"title": "Website URL inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "https://www.diamondspringsdental.com/",
|
||||
"apple": "http://www.diamondspringsdental.com"
|
||||
},
|
||||
"recommendation": "Use one canonical URL (prefer HTTPS, no www) across all directories."
|
||||
},
|
||||
{
|
||||
"id": "missing_jsonld_schema",
|
||||
"severity": "high",
|
||||
"title": "No JSON-LD schema found on website",
|
||||
"sub_score": "Technical SEO",
|
||||
"evidence": {
|
||||
"url": "https://www.diamondspringsdental.com/",
|
||||
"has_jsonld": false
|
||||
},
|
||||
"recommendation": "Add LocalBusiness/BeautySalon JSON-LD schema. Critical for rich results and local ranking."
|
||||
},
|
||||
{
|
||||
"id": "review_count_delta",
|
||||
"severity": "medium",
|
||||
"title": "Review count varies by 21 across surfaces",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"google": 49,
|
||||
"apple": 28
|
||||
},
|
||||
"recommendation": "Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership."
|
||||
},
|
||||
{
|
||||
"id": "review_dates_unavailable",
|
||||
"severity": "medium",
|
||||
"title": "Cannot determine review recency \u2014 no dates available in samples",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sources_with_dates": []
|
||||
},
|
||||
"recommendation": "Review date data is not available from current surfaces. Monitor via GBP dashboard directly."
|
||||
},
|
||||
{
|
||||
"id": "category_fragmentation",
|
||||
"severity": "medium",
|
||||
"title": "Category varies across surfaces: dental clinic, health care",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "dental clinic",
|
||||
"apple": "health care"
|
||||
},
|
||||
"recommendation": "Align primary category across all directories. GBP primary category is the ranking driver."
|
||||
},
|
||||
{
|
||||
"id": "reviews_low_specificity",
|
||||
"severity": "enhancement",
|
||||
"title": "Only 0% of sample reviews mention specific services or practitioners",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
},
|
||||
"recommendation": "Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better."
|
||||
},
|
||||
{
|
||||
"id": "no_price_level",
|
||||
"severity": "enhancement",
|
||||
"title": "Price level not set on GBP or Apple Maps",
|
||||
"sub_score": "Content",
|
||||
"evidence": {},
|
||||
"recommendation": "Set price level ($\u2013$$$$) on GBP. Helps users self-select and improves relevance."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
# Audit Findings — Diamond Springs Dental Center
|
||||
|
||||
**Generated:** 2026-08-15 13:56 UTC
|
||||
**Sources checked:** google_business_profile, apple_maps, website
|
||||
**Rating:** 3.4 | **Reviews:** 49
|
||||
|
||||
## Severity Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🟥 Immediate | 4 |
|
||||
| 🟧 High | 2 |
|
||||
| 🟨 Medium | 3 |
|
||||
| 🟩 Enhancement | 2 |
|
||||
| **Total** | **11** |
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding #1: Phone number inconsistent across 2 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize phone format across all directories. GBP is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "(530) 291-1888",
|
||||
"apple": "+1 (530) 344-0290",
|
||||
"website": "551-0858478"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #2: Address inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize exact address format. GBP address is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "6400 Pleasant Valley Rd, El Dorado, CA 95623",
|
||||
"apple": "493 Pleasant Valley Rd, Ste 110, Diamond Springs, CA 95619"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #3: Hours mismatch on 4 day(s) — GBP is authoritative
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Google Business
|
||||
- **Recommendation:** Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss.
|
||||
- **Evidence:** ```json
|
||||
[
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Finding #4: Average rating 3.3 — below 4.0 threshold
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Address negative reviews, request new reviews from satisfied customers. Below 4.0 is a strong conversion blocker.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"average": 3.35,
|
||||
"by_source": {
|
||||
"google": 3.4,
|
||||
"apple": 3.3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #5: Website URL inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Use one canonical URL (prefer HTTPS, no www) across all directories.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "https://www.diamondspringsdental.com/",
|
||||
"apple": "http://www.diamondspringsdental.com"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #6: No JSON-LD schema found on website
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Technical SEO
|
||||
- **Recommendation:** Add LocalBusiness/BeautySalon JSON-LD schema. Critical for rich results and local ranking.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"url": "https://www.diamondspringsdental.com/",
|
||||
"has_jsonld": false
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #7: Review count varies by 21 across surfaces
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": 49,
|
||||
"apple": 28
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #8: Cannot determine review recency — no dates available in samples
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Review date data is not available from current surfaces. Monitor via GBP dashboard directly.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sources_with_dates": []
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #9: Category varies across surfaces: dental clinic, health care
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Align primary category across all directories. GBP primary category is the ranking driver.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "dental clinic",
|
||||
"apple": "health care"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #10: Only 0% of sample reviews mention specific services or practitioners
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #11: Price level not set on GBP or Apple Maps
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Set price level ($–$$$$) on GBP. Helps users self-select and improves relevance.
|
||||
|
||||
## Failed Surfaces
|
||||
|
||||
- ❌ `bing_places` — no data returned
|
||||
@@ -0,0 +1,24 @@
|
||||
# Delta: Gilmore Heating, Air and Plumbing
|
||||
|
||||
**Before:** 15 August 2026 (`gilmore_heating_air_and_plumbing_multi_surface_2026-08-15.json`)
|
||||
**After:** 15 August 2026 (`gilmore_heating_air_and_plumbing_multi_surface_2026-08-15__20260815-164620.json`)
|
||||
|
||||
**0 field(s) changed** across the audited surfaces.
|
||||
|
||||
No changes detected between the two captures.
|
||||
|
||||
## Unchanged
|
||||
|
||||
- Name (GBP): Gilmore Heating, Air and Plumbing
|
||||
- Phone (GBP): 5302905551
|
||||
- Phone (Apple): 9168665596
|
||||
- Address (GBP): 8636 antelope north rd bldg d antelope ca 95843
|
||||
- Address (Apple): 8636 antelope north rd antelope ca 95843
|
||||
- Website (GBP): https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca
|
||||
- Rating (GBP): 4.8
|
||||
- Reviews (GBP): 3860
|
||||
- Category (GBP): HVAC contractor
|
||||
- Description set (GBP): False
|
||||
- Schema (website): False
|
||||
|
||||
*Generated by audit_diff.py (locked process v1.1). Compare only — no judgment.*
|
||||
@@ -0,0 +1,155 @@
|
||||
# Gilmore Heating, Air and Plumbing
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** Antelope, CA
|
||||
**Vertical:** HVAC
|
||||
**Audit date:** 15 August 2026
|
||||
**Prepared for:** Gilmore Heating, Air and Plumbing
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited Gilmore Heating, Air and Plumbing across its primary online surfaces: Google Business Profile and Apple Maps. The business shows 4.8 ★ across 3,860 Google reviews.
|
||||
**8 material integrity issues were identified:**
|
||||
|
||||
1. Phone numbers are inconsistent across directories.
|
||||
2. Address details differ between Google and Apple.
|
||||
3. Business hours are inconsistent between Google and Apple.
|
||||
5. The website URL does not align across directories.
|
||||
4. Average rating is below the 4.0 trust threshold.
|
||||
7. No website could be located for this business.
|
||||
6. Ratings vary across surfaces.
|
||||
8. No business description is set on any directory.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✓ | 4.8 ★ | 3,860 |
|
||||
| Apple Maps | ✓ | 2.5 ★ | 272 |
|
||||
| Bing Places | ✗ | capture blocked (bot-wall) — excluded, not verified | — |
|
||||
| Website | ✗ | not found / unreachable at audit time | — |
|
||||
|
||||
> **Limitation:** The website could not be reached or located at audit time. A blocked or missing website is itself a customer-path risk: visitors arriving from search cannot engage the business. Reachability should be re-verified; if the block persists, treat it as a material finding in the next run.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Phone number inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists **(530) 290-5551**. Apple lists **+1 (916) 866-5596**.
|
||||
|
||||
**Impact:**
|
||||
Customers reaching the business through a single directory may call a disconnected or wrong number. Inconsistent NAP (name-address-phone) signals reduce local search ranking across all surfaces.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the correct primary number, then update every directory to match. Keep the verified number identical across Google, Apple, and the website.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: Address inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists “8636 Antelope North Rd Bldg D, Antelope, CA 95843”. Apple Maps lists “8636 Antelope North Rd, Antelope, CA 95843”.
|
||||
|
||||
**Impact:**
|
||||
Conflicting addresses split local search signals and can send customers or service vehicles to the wrong location. NAP consistency is a core local-ranking factor.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the canonical street address (including unit) with the business, then align Google and Apple to the same string.
|
||||
|
||||
---
|
||||
|
||||
### Finding 3: Hours mismatch between Google and Apple
|
||||
|
||||
**Observation:**
|
||||
saturday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; sunday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; monday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; tuesday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; wednesday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; thursday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”; friday: Google “Open 24 hours” vs Apple “12:00 AM-12:00 PM”
|
||||
|
||||
**Impact:**
|
||||
Customers planning a visit see different availability depending on which directory they use. Mismatched hours drive no-shows and lost calls at the edge of the workday.
|
||||
|
||||
**Recommendation:**
|
||||
Verify actual operating hours with the business. Update the non-authoritative surface to match Google's verified schedule, or correct Google if its hours are inaccurate.
|
||||
|
||||
---
|
||||
|
||||
### Finding 4: Website URL inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists `https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca`. Apple Maps lists `https://gilmoreair.com`.
|
||||
|
||||
**Impact:**
|
||||
Divergent website URLs split SEO equity and distort analytics attribution; link signals may not consolidate on the canonical domain.
|
||||
|
||||
**Recommendation:**
|
||||
Point every directory at the canonical website URL (https, no tracking parameters) and consolidate redirects so link equity consolidates on one domain.
|
||||
|
||||
---
|
||||
|
||||
### Finding 5: Average rating 3.6 — below 4.0 threshold
|
||||
|
||||
**Observation:**
|
||||
Blended average is **3.65** (Google 4.8, Apple 2.5) — below the 4.0 trust threshold.
|
||||
|
||||
**Impact:**
|
||||
A sub-4.0 average is below the trust threshold for most local verticals and suppresses click-through in the local pack regardless of review volume.
|
||||
|
||||
**Recommendation:**
|
||||
Review the lower-rated reviews for recurring service issues; respond publicly to negative reviews and prioritize the service gaps they describe.
|
||||
|
||||
---
|
||||
|
||||
### Finding 6: Website listed on GBP but unreachable at audit time
|
||||
|
||||
**Observation:**
|
||||
GBP website='https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca' — surface blocked; documented limitation per process §4
|
||||
|
||||
**Impact:**
|
||||
Without a website, the business cedes its owned property: no service pages, no schema, no conversion path beyond a phone call. AI search surfaces increasingly require a website to cite.
|
||||
|
||||
**Recommendation:**
|
||||
Stand up (or link on GBP) a business website with service pages, contact details, and LocalBusiness schema.
|
||||
|
||||
---
|
||||
|
||||
### Finding 7: Rating varies by 2.3 across surfaces (range: 2.5–4.8)
|
||||
|
||||
**Observation:**
|
||||
Google shows **4.8** ★ while Apple Maps shows **2.5** ★.
|
||||
|
||||
**Impact:**
|
||||
A rating spread across surfaces undermines the consistency signal local search relies on and can suppress the lower-rated surface in results.
|
||||
|
||||
**Recommendation:**
|
||||
Verify which rating is current; the stale surface usually reflects an unmanaged listing that has stopped receiving reviews.
|
||||
|
||||
---
|
||||
|
||||
### Finding 8: No business description found on any surface
|
||||
|
||||
**Observation:**
|
||||
no description on any checked surface — weakens local-pack snippet
|
||||
|
||||
**Impact:**
|
||||
An empty description leaves the local-pack snippet to algorithmic default and forfeits the business's own service language in search results.
|
||||
|
||||
**Recommendation:**
|
||||
Write a 2-3 sentence GBP description covering services, service area, and differentiators.
|
||||
|
||||
---
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; ratings and counts only.
|
||||
- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*
|
||||
@@ -0,0 +1,59 @@
|
||||
# Validation Run: Gilmore Heating, Air and Plumbing
|
||||
|
||||
**Date:** 15 August 2026
|
||||
**Business:** Gilmore Heating, Air and Plumbing
|
||||
**Rating:** 4.8 ★ | **Reviews:** 3860
|
||||
**Engine findings:** 13 | **Material (in report):** 8 | **Rejected FP/low-signal:** 3 | **Needs verification:** 0
|
||||
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
### Material (survive normalization → in REPORT-final.md)
|
||||
|
||||
- **immediate** — Phone number inconsistent across 1 surface(s)
|
||||
`distinct numbers: 5302905551, 9168665596`
|
||||
- **immediate** — Address inconsistent across 1 surface(s)
|
||||
`google='8636 Antelope North Rd Bldg D, Antelope, CA 95843' vs apple='8636 Antelope North Rd, Antelope, CA 95843'`
|
||||
- **immediate** — Hours mismatch on 7 day(s) — GBP is authoritative
|
||||
`7 day(s) genuinely differ: saturday: Open 24 hours vs 12:00 AM-12:00 PM; sunday: Open 24 hours vs 12:00 AM-12:00 PM; monday: Open 24 hours vs 12:00 AM-12:00 PM; tuesday: Open 24 hours vs 12:00 AM-12:00 PM`
|
||||
- **immediate** — Average rating 3.6 — below 4.0 threshold
|
||||
`average 3.65 (google 4.8 / apple 2.5)`
|
||||
- **high** — Website URL inconsistent across 1 surface(s)
|
||||
`google='https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca' vs apple='https://gilmoreair.com' — different domains`
|
||||
- **high** — Rating varies by 2.3 across surfaces (range: 2.5–4.8)
|
||||
`google 4.8 vs apple 2.5`
|
||||
- **high** — Website listed on GBP but unreachable at audit time
|
||||
`GBP website='https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca' — surface blocked; documented limitation per process §4`
|
||||
- **medium** — No business description found on any surface
|
||||
`no description on any checked surface — weakens local-pack snippet`
|
||||
|
||||
### Needs verification (excluded from report until confirmed)
|
||||
|
||||
- none
|
||||
|
||||
### Rejected (format false-positives / low-signal / tooling gaps)
|
||||
|
||||
- ~~Review count varies by 3588 across surfaces~~ — low-signal: platforms count reviews differently; not an integrity issue
|
||||
- ~~Cannot determine review recency — no dates available in samples~~ — tooling gap: scraper captures no review dates (documented limitation)
|
||||
- ~~Category varies across surfaces: consumer sector, hvac contractor~~ — low-signal: per-platform taxonomy (Apple 'consumer sector' is its default)
|
||||
|
||||
### Enhancement tier (noted, excluded from client report)
|
||||
|
||||
- Only 0% of sample reviews mention specific services or practitioners
|
||||
- Price level not set on GBP or Apple Maps
|
||||
|
||||
## Pre-delivery consistency gate (v1.1 §6)
|
||||
- Count match: executive summary states 8; Findings section contains 8 ✅
|
||||
- Evidence present: every cited value exists in findings.json / raw capture ✅
|
||||
- Identity match: name, location, and date match intake record ✅
|
||||
|
||||
*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*
|
||||
|
||||
## GATE RESULT
|
||||
|
||||
**PASS** — report_gate.py, 2026-08-15T16:52:53
|
||||
|
||||
- Count: 8 stated / 8 material after review — match
|
||||
- Identity: report title + date match raw capture ('Gilmore Heating, Air and Plumbing', 15 August 2026) — match
|
||||
- Surface disclosure: all captured/failed surfaces rendered correctly — match
|
||||
- Material support: every material finding re-derived from raw capture — hold
|
||||
- Needs-verification open: 0
|
||||
@@ -0,0 +1,208 @@
|
||||
{
|
||||
"audit_engine": "v1",
|
||||
"timestamp": "2026-08-15T16:46:20.939734+00:00",
|
||||
"business": "Gilmore Heating, Air and Plumbing",
|
||||
"summary": {
|
||||
"total_findings": 13,
|
||||
"severity_counts": {
|
||||
"immediate": 4,
|
||||
"high": 3,
|
||||
"medium": 4,
|
||||
"enhancement": 2
|
||||
},
|
||||
"sub_score_issues": {
|
||||
"Digital Identity": 4,
|
||||
"Google Business": 1,
|
||||
"Reviews": 5,
|
||||
"Technical SEO": 1,
|
||||
"Content": 2
|
||||
},
|
||||
"surfaces_ok": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true
|
||||
},
|
||||
"surfaces_failed": {
|
||||
"bing_places": false,
|
||||
"website": false
|
||||
},
|
||||
"rating": 4.8,
|
||||
"reviews": 3860
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "nap_phone_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Phone number inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "(530) 290-5551",
|
||||
"apple": "+1 (916) 866-5596"
|
||||
},
|
||||
"recommendation": "Standardize phone format across all directories. GBP is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "nap_address_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Address inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "8636 Antelope North Rd Bldg D, Antelope, CA 95843",
|
||||
"apple": "8636 Antelope North Rd, Antelope, CA 95843"
|
||||
},
|
||||
"recommendation": "Standardize exact address format. GBP address is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "hours_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Hours mismatch on 7 day(s) \u2014 GBP is authoritative",
|
||||
"sub_score": "Google Business",
|
||||
"evidence": [
|
||||
{
|
||||
"day": "saturday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "sunday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
}
|
||||
],
|
||||
"recommendation": "Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."
|
||||
},
|
||||
{
|
||||
"id": "rating_below_threshold",
|
||||
"severity": "immediate",
|
||||
"title": "Average rating 3.6 \u2014 below 4.0 threshold",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"average": 3.65,
|
||||
"by_source": {
|
||||
"google": 4.8,
|
||||
"apple": 2.5
|
||||
}
|
||||
},
|
||||
"recommendation": "Address negative reviews, request new reviews from satisfied customers. Below 4.0 is a strong conversion blocker."
|
||||
},
|
||||
{
|
||||
"id": "nap_website_mismatch",
|
||||
"severity": "high",
|
||||
"title": "Website URL inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca",
|
||||
"apple": "https://gilmoreair.com"
|
||||
},
|
||||
"recommendation": "Use one canonical URL (prefer HTTPS, no www) across all directories."
|
||||
},
|
||||
{
|
||||
"id": "rating_delta_high",
|
||||
"severity": "high",
|
||||
"title": "Rating varies by 2.3 across surfaces (range: 2.5\u20134.8)",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"google": 4.8,
|
||||
"apple": 2.5
|
||||
},
|
||||
"recommendation": "Investigate why some surfaces show different ratings. May indicate stale data or mixed-up business listings."
|
||||
},
|
||||
{
|
||||
"id": "no_website",
|
||||
"severity": "high",
|
||||
"title": "No website found",
|
||||
"sub_score": "Technical SEO",
|
||||
"evidence": {},
|
||||
"recommendation": "A website is essential for credibility, schema markup, and conversion. Launch one immediately."
|
||||
},
|
||||
{
|
||||
"id": "review_count_delta",
|
||||
"severity": "medium",
|
||||
"title": "Review count varies by 3588 across surfaces",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"google": 3860,
|
||||
"apple": 272
|
||||
},
|
||||
"recommendation": "Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership."
|
||||
},
|
||||
{
|
||||
"id": "review_dates_unavailable",
|
||||
"severity": "medium",
|
||||
"title": "Cannot determine review recency \u2014 no dates available in samples",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sources_with_dates": []
|
||||
},
|
||||
"recommendation": "Review date data is not available from current surfaces. Monitor via GBP dashboard directly."
|
||||
},
|
||||
{
|
||||
"id": "category_fragmentation",
|
||||
"severity": "medium",
|
||||
"title": "Category varies across surfaces: consumer sector, hvac contractor",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "hvac contractor",
|
||||
"apple": "consumer sector"
|
||||
},
|
||||
"recommendation": "Align primary category across all directories. GBP primary category is the ranking driver."
|
||||
},
|
||||
{
|
||||
"id": "no_description",
|
||||
"severity": "medium",
|
||||
"title": "No business description found on any surface",
|
||||
"sub_score": "Content",
|
||||
"evidence": {
|
||||
"surfaces_checked": [
|
||||
"google",
|
||||
"bing",
|
||||
"website"
|
||||
]
|
||||
},
|
||||
"recommendation": "Add a keyword-rich description to GBP and website. Descriptions improve click-through and category relevance."
|
||||
},
|
||||
{
|
||||
"id": "reviews_low_specificity",
|
||||
"severity": "enhancement",
|
||||
"title": "Only 0% of sample reviews mention specific services or practitioners",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
},
|
||||
"recommendation": "Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better."
|
||||
},
|
||||
{
|
||||
"id": "no_price_level",
|
||||
"severity": "enhancement",
|
||||
"title": "Price level not set on GBP or Apple Maps",
|
||||
"sub_score": "Content",
|
||||
"evidence": {},
|
||||
"recommendation": "Set price level ($\u2013$$$$) on GBP. Helps users self-select and improves relevance."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
# Audit Findings — Gilmore Heating, Air and Plumbing
|
||||
|
||||
**Generated:** 2026-08-15 16:46 UTC
|
||||
**Sources checked:** google_business_profile, apple_maps
|
||||
**Rating:** 4.8 | **Reviews:** 3860
|
||||
|
||||
## Severity Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🟥 Immediate | 4 |
|
||||
| 🟧 High | 3 |
|
||||
| 🟨 Medium | 4 |
|
||||
| 🟩 Enhancement | 2 |
|
||||
| **Total** | **13** |
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding #1: Phone number inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize phone format across all directories. GBP is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "(530) 290-5551",
|
||||
"apple": "+1 (916) 866-5596"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #2: Address inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize exact address format. GBP address is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "8636 Antelope North Rd Bldg D, Antelope, CA 95843",
|
||||
"apple": "8636 Antelope North Rd, Antelope, CA 95843"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #3: Hours mismatch on 7 day(s) — GBP is authoritative
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Google Business
|
||||
- **Recommendation:** Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss.
|
||||
- **Evidence:** ```json
|
||||
[
|
||||
{
|
||||
"day": "saturday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "sunday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "12:00 AM-12:00 PM"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Finding #4: Average rating 3.6 — below 4.0 threshold
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Address negative reviews, request new reviews from satisfied customers. Below 4.0 is a strong conversion blocker.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"average": 3.65,
|
||||
"by_source": {
|
||||
"google": 4.8,
|
||||
"apple": 2.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #5: Website URL inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Use one canonical URL (prefer HTTPS, no www) across all directories.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca",
|
||||
"apple": "https://gilmoreair.com"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #6: Rating varies by 2.3 across surfaces (range: 2.5–4.8)
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Investigate why some surfaces show different ratings. May indicate stale data or mixed-up business listings.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": 4.8,
|
||||
"apple": 2.5
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #7: No website found
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Technical SEO
|
||||
- **Recommendation:** A website is essential for credibility, schema markup, and conversion. Launch one immediately.
|
||||
|
||||
### Finding #8: Review count varies by 3588 across surfaces
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": 3860,
|
||||
"apple": 272
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #9: Cannot determine review recency — no dates available in samples
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Review date data is not available from current surfaces. Monitor via GBP dashboard directly.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sources_with_dates": []
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #10: Category varies across surfaces: consumer sector, hvac contractor
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Align primary category across all directories. GBP primary category is the ranking driver.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "hvac contractor",
|
||||
"apple": "consumer sector"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #11: No business description found on any surface
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Add a keyword-rich description to GBP and website. Descriptions improve click-through and category relevance.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"surfaces_checked": [
|
||||
"google",
|
||||
"bing",
|
||||
"website"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #12: Only 0% of sample reviews mention specific services or practitioners
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #13: Price level not set on GBP or Apple Maps
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Set price level ($–$$$$) on GBP. Helps users self-select and improves relevance.
|
||||
|
||||
## Failed Surfaces
|
||||
|
||||
- ❌ `bing_places` — no data returned
|
||||
- ❌ `website` — no data returned
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"audit": {
|
||||
"timestamp": "2026-08-15T14:02:25.454316+00:00",
|
||||
"tool": "multi_scraper_v1",
|
||||
"surfaces_checked": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": true,
|
||||
"website": false
|
||||
},
|
||||
"verification": {
|
||||
"name_mismatch": {
|
||||
"primary": "Gilmore Heating, Air and Plumbing",
|
||||
"conflicts": {
|
||||
"apple": "Gilmore Heating, Air And Plumbing"
|
||||
}
|
||||
},
|
||||
"phone_mismatch": {
|
||||
"primary": "(530) 290-5551",
|
||||
"conflicts": {
|
||||
"apple": "+1 (916) 866-5596"
|
||||
}
|
||||
},
|
||||
"website_mismatch": {
|
||||
"primary": "https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca",
|
||||
"conflicts": {
|
||||
"apple": "https://gilmoreair.com"
|
||||
}
|
||||
},
|
||||
"hours_mismatch": {
|
||||
"saturday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"sunday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"monday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"tuesday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"wednesday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"thursday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"friday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"primary_source": "google_business_profile",
|
||||
"name": "Gilmore Heating, Air and Plumbing",
|
||||
"address": "8636 Antelope North Rd Bldg D, Antelope, CA 95843",
|
||||
"phone": "(530) 290-5551",
|
||||
"website": "https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca",
|
||||
"rating": 4.8,
|
||||
"reviews": 3860,
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Gilmore repaired a leaky pipe under my bathroom wash basin. Before installing, I asked how much was it. They said $531. They did the work but had to come back the next day to use plumber's tape to fix",
|
||||
"rating": 2
|
||||
},
|
||||
{
|
||||
"text": "The work was good, but the pricing is outrageous. I wish I would've read the reviews prior to agreeing to the service. Make sure you shop around.",
|
||||
"rating": 2
|
||||
},
|
||||
{
|
||||
"text": "If you schedule an appointment with them, it will be a full day window, and there's no guarantee they'll show up until after that 8-5 window. If you call OR schedule online for a specific fix (in my c",
|
||||
"rating": 1
|
||||
}
|
||||
],
|
||||
"hours": {
|
||||
"saturday": "Open 24 hours",
|
||||
"sunday": "Open 24 hours",
|
||||
"monday": "Open 24 hours",
|
||||
"tuesday": "Open 24 hours",
|
||||
"wednesday": "Open 24 hours",
|
||||
"thursday": "Open 24 hours",
|
||||
"friday": "Open 24 hours"
|
||||
},
|
||||
"category": "HVAC contractor",
|
||||
"price_level": null,
|
||||
"photos_count": null,
|
||||
"description": null,
|
||||
"coordinates": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
},
|
||||
"closed_status": {
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"sources": {
|
||||
"google_business_profile": {
|
||||
"name": "Gilmore Heating, Air and Plumbing",
|
||||
"address": "8636 Antelope North Rd Bldg D, Antelope, CA 95843",
|
||||
"phone": "(530) 290-5551",
|
||||
"website": "https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca",
|
||||
"rating": 4.8,
|
||||
"reviews": 3860,
|
||||
"hours": {
|
||||
"saturday": "Open 24 hours",
|
||||
"sunday": "Open 24 hours",
|
||||
"monday": "Open 24 hours",
|
||||
"tuesday": "Open 24 hours",
|
||||
"wednesday": "Open 24 hours",
|
||||
"thursday": "Open 24 hours",
|
||||
"friday": "Open 24 hours"
|
||||
},
|
||||
"category": "HVAC contractor",
|
||||
"price_level": null,
|
||||
"description": null,
|
||||
"photos_count": null,
|
||||
"latitude": null,
|
||||
"longitude": null,
|
||||
"url": "https://www.google.com/maps/search/Gilmore%20Heating%20Air%20and%20Plumbing%20Placerville%2C%20CA/data=!3m1!4b1",
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"apple_maps": {
|
||||
"address": "8636 Antelope North Rd, Antelope, CA 95843",
|
||||
"hours": {
|
||||
"sunday": "12:00 AM-12:00 PM",
|
||||
"monday": "12:00 AM-12:00 PM",
|
||||
"tuesday": "12:00 AM-12:00 PM",
|
||||
"wednesday": "12:00 AM-12:00 PM",
|
||||
"thursday": "12:00 AM-12:00 PM",
|
||||
"friday": "12:00 AM-12:00 PM",
|
||||
"saturday": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"rating": 2.5,
|
||||
"reviews": 272,
|
||||
"coordinates": {
|
||||
"lat": 38.7186405,
|
||||
"lon": -121.3270005
|
||||
},
|
||||
"name": "Gilmore Heating, Air And Plumbing",
|
||||
"category": "Consumer Sector",
|
||||
"phone": "+1 (916) 866-5596",
|
||||
"website": "https://gilmoreair.com",
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Gilmore repaired a leaky pipe under my bathroom wash basin. Before installing, I asked how much was it. They said $531. They did the work but had to come back the next day to use plumber's tape to fix",
|
||||
"rating": 2
|
||||
},
|
||||
{
|
||||
"text": "The work was good, but the pricing is outrageous. I wish I would've read the reviews prior to agreeing to the service. Make sure you shop around.",
|
||||
"rating": 2
|
||||
},
|
||||
{
|
||||
"text": "If you schedule an appointment with them, it will be a full day window, and there's no guarantee they'll show up until after that 8-5 window. If you call OR schedule online for a specific fix (in my c",
|
||||
"rating": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"bing_places": {
|
||||
"name": "Gilmore Heating, Air & Plumbing",
|
||||
"website": "https://gilmoreair.com",
|
||||
"address": "4429 Missouri Flat Rd, Placerville, CA 95667"
|
||||
},
|
||||
"website": null
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"audit": {
|
||||
"timestamp": "2026-08-15T16:46:19.861071+00:00",
|
||||
"tool": "multi_scraper_v1",
|
||||
"surfaces_checked": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": false,
|
||||
"website": false
|
||||
},
|
||||
"verification": {
|
||||
"name_mismatch": {
|
||||
"primary": "Gilmore Heating, Air and Plumbing",
|
||||
"conflicts": {
|
||||
"apple": "Gilmore Heating, Air And Plumbing"
|
||||
}
|
||||
},
|
||||
"phone_mismatch": {
|
||||
"primary": "(530) 290-5551",
|
||||
"conflicts": {
|
||||
"apple": "+1 (916) 866-5596"
|
||||
}
|
||||
},
|
||||
"website_mismatch": {
|
||||
"primary": "https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca",
|
||||
"conflicts": {
|
||||
"apple": "https://gilmoreair.com"
|
||||
}
|
||||
},
|
||||
"hours_mismatch": {
|
||||
"saturday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"sunday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"monday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"tuesday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"wednesday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"thursday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"friday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "12:00 AM-12:00 PM"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"primary_source": "google_business_profile",
|
||||
"name": "Gilmore Heating, Air and Plumbing",
|
||||
"address": "8636 Antelope North Rd Bldg D, Antelope, CA 95843",
|
||||
"phone": "(530) 290-5551",
|
||||
"website": "https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca",
|
||||
"rating": 4.8,
|
||||
"reviews": 3860,
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Gilmore repaired a leaky pipe under my bathroom wash basin. Before installing, I asked how much was it. They said $531. They did the work but had to come back the next day to use plumber's tape to fix",
|
||||
"rating": 2
|
||||
},
|
||||
{
|
||||
"text": "The work was good, but the pricing is outrageous. I wish I would've read the reviews prior to agreeing to the service. Make sure you shop around.",
|
||||
"rating": 2
|
||||
},
|
||||
{
|
||||
"text": "If you schedule an appointment with them, it will be a full day window, and there's no guarantee they'll show up until after that 8-5 window. If you call OR schedule online for a specific fix (in my c",
|
||||
"rating": 1
|
||||
}
|
||||
],
|
||||
"hours": {
|
||||
"saturday": "Open 24 hours",
|
||||
"sunday": "Open 24 hours",
|
||||
"monday": "Open 24 hours",
|
||||
"tuesday": "Open 24 hours",
|
||||
"wednesday": "Open 24 hours",
|
||||
"thursday": "Open 24 hours",
|
||||
"friday": "Open 24 hours"
|
||||
},
|
||||
"category": "HVAC contractor",
|
||||
"price_level": null,
|
||||
"photos_count": null,
|
||||
"description": null,
|
||||
"coordinates": {
|
||||
"lat": 38.7186718,
|
||||
"lon": -121.3265518
|
||||
},
|
||||
"closed_status": {
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"sources": {
|
||||
"google_business_profile": {
|
||||
"name": "Gilmore Heating, Air and Plumbing",
|
||||
"address": "8636 Antelope North Rd Bldg D, Antelope, CA 95843",
|
||||
"phone": "(530) 290-5551",
|
||||
"website": "https://gilmoreair.com/service-areas/antelope/?utm_source=GBP-listing&utm_medium=organic&utm_campaign=antelope-ca",
|
||||
"rating": 4.8,
|
||||
"reviews": 3860,
|
||||
"hours": {
|
||||
"saturday": "Open 24 hours",
|
||||
"sunday": "Open 24 hours",
|
||||
"monday": "Open 24 hours",
|
||||
"tuesday": "Open 24 hours",
|
||||
"wednesday": "Open 24 hours",
|
||||
"thursday": "Open 24 hours",
|
||||
"friday": "Open 24 hours"
|
||||
},
|
||||
"category": "HVAC contractor",
|
||||
"price_level": null,
|
||||
"description": null,
|
||||
"photos_count": null,
|
||||
"latitude": 38.7186718,
|
||||
"longitude": -121.3265518,
|
||||
"url": "https://www.google.com/maps/place/Gilmore+Heating,+Air+and+Plumbing/@38.7186718,-121.3265518,17z/data=!3m1!4b1!4m6!3m5!1s0x809b21e778a000f3:0x5e737079a3661740!8m2!3d38.7186718!4d-121.3265518!16s%2Fg%2F11nl7ls20_?entry=ttu&g_ep=EgoyMDI2MDgxMi4wIKXMDSoASAFQAw%3D%3D",
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"apple_maps": {
|
||||
"address": "8636 Antelope North Rd, Antelope, CA 95843",
|
||||
"hours": {
|
||||
"sunday": "12:00 AM-12:00 PM",
|
||||
"monday": "12:00 AM-12:00 PM",
|
||||
"tuesday": "12:00 AM-12:00 PM",
|
||||
"wednesday": "12:00 AM-12:00 PM",
|
||||
"thursday": "12:00 AM-12:00 PM",
|
||||
"friday": "12:00 AM-12:00 PM",
|
||||
"saturday": "12:00 AM-12:00 PM"
|
||||
},
|
||||
"rating": 2.5,
|
||||
"reviews": 272,
|
||||
"coordinates": {
|
||||
"lat": 38.7186405,
|
||||
"lon": -121.3270005
|
||||
},
|
||||
"name": "Gilmore Heating, Air And Plumbing",
|
||||
"category": "Consumer Sector",
|
||||
"phone": "+1 (916) 866-5596",
|
||||
"website": "https://gilmoreair.com",
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "Gilmore repaired a leaky pipe under my bathroom wash basin. Before installing, I asked how much was it. They said $531. They did the work but had to come back the next day to use plumber's tape to fix",
|
||||
"rating": 2
|
||||
},
|
||||
{
|
||||
"text": "The work was good, but the pricing is outrageous. I wish I would've read the reviews prior to agreeing to the service. Make sure you shop around.",
|
||||
"rating": 2
|
||||
},
|
||||
{
|
||||
"text": "If you schedule an appointment with them, it will be a full day window, and there's no guarantee they'll show up until after that 8-5 window. If you call OR schedule online for a specific fix (in my c",
|
||||
"rating": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"bing_places": null,
|
||||
"website": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# Golden Hills Family Dental
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** El Dorado Hills, CA
|
||||
**Vertical:** Dental
|
||||
**Audit date:** 15 August 2026
|
||||
**Prepared for:** Golden Hills Family Dental
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited Golden Hills Family Dental across its primary online surfaces: Google Business Profile, Apple Maps and Website. The business shows 5.0 ★ across 283 Google reviews.
|
||||
**2 material integrity issues were identified:**
|
||||
|
||||
1. Phone numbers are inconsistent across directories.
|
||||
2. The website lacks LocalBusiness structured data.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✓ | 5.0 ★ | 283 |
|
||||
| Apple Maps | ✓ | 4.7 ★ | 41 |
|
||||
| Bing Places | ✗ | capture blocked (bot-wall) — excluded, not verified | — |
|
||||
| Website | ✓ | — | — |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Phone number inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists **(916) 939-0889**. Apple lists **+1 (916) 939-0889**. Website lists **6389012842**.
|
||||
|
||||
**Impact:**
|
||||
Customers reaching the business through a single directory may call a disconnected or wrong number. Inconsistent NAP (name-address-phone) signals reduce local search ranking across all surfaces.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the correct primary number, then update every directory to match. Keep the verified number identical across Google, Apple, and the website.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: No JSON-LD schema found on website
|
||||
|
||||
**Observation:**
|
||||
{"url": "http://www.goldenhillsfamilydental.net/", "has_jsonld": false}
|
||||
|
||||
**Impact:**
|
||||
Without LocalBusiness structured data, AI assistants and structured search cannot reliably classify or cite the business's services, hours, and area served.
|
||||
|
||||
**Recommendation:**
|
||||
Add LocalBusiness JSON-LD (Dentist/Plumber/Contractor type) with name, address, phone, hours, and areaServed.
|
||||
|
||||
---
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; ratings and counts only.
|
||||
- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*
|
||||
@@ -0,0 +1,50 @@
|
||||
# Validation Run: Golden Hills Family Dental
|
||||
|
||||
**Date:** 15 August 2026
|
||||
**Business:** Golden Hills Family Dental
|
||||
**Rating:** 5.0 ★ | **Reviews:** 283
|
||||
**Engine findings:** 10 | **Material (in report):** 2 | **Rejected FP/low-signal:** 5 | **Needs verification:** 0
|
||||
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
### Material (survive normalization → in REPORT-final.md)
|
||||
|
||||
- **immediate** — Phone number inconsistent across 1 surface(s)
|
||||
`distinct numbers: 6389012842, 9169390889`
|
||||
- **high** — No JSON-LD schema found on website
|
||||
`no structured data at http://www.goldenhillsfamilydental.net/`
|
||||
|
||||
### Needs verification (excluded from report until confirmed)
|
||||
|
||||
- none
|
||||
|
||||
### Rejected (format false-positives / low-signal / tooling gaps)
|
||||
|
||||
- ~~Address inconsistent across 1 surface(s)~~ — same street/city/zip; suite format only (# vs Ste vs Unit)
|
||||
- ~~Hours mismatch on 5 day(s) — GBP is authoritative~~ — format only: '8 AM' vs '8:00 AM' — identical after parse
|
||||
- ~~Review count varies by 242 across surfaces~~ — low-signal: platforms count reviews differently; not an integrity issue
|
||||
- ~~Cannot determine review recency — no dates available in samples~~ — tooling gap: scraper captures no review dates (documented limitation)
|
||||
- ~~Category varies across surfaces: cosmetic dentist, health care~~ — low-signal: per-platform taxonomy (Apple 'consumer sector' is its default)
|
||||
|
||||
### Enhancement tier (noted, excluded from client report)
|
||||
|
||||
- Only 0% of sample reviews mention specific services or practitioners
|
||||
- Price level not set on GBP or Apple Maps
|
||||
- No Open Graph tags on website
|
||||
|
||||
## Pre-delivery consistency gate (v1.1 §6)
|
||||
- Count match: executive summary states 2; Findings section contains 2 ✅
|
||||
- Evidence present: every cited value exists in findings.json / raw capture ✅
|
||||
- Identity match: name, location, and date match intake record ✅
|
||||
|
||||
*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*
|
||||
|
||||
## GATE RESULT
|
||||
|
||||
**PASS** — report_gate.py, 2026-08-15T16:27:11
|
||||
|
||||
- Count: 2 stated / 2 material after review — match
|
||||
- Identity: report title + date match raw capture ('Golden Hills Family Dental', 15 August 2026) — match
|
||||
- Surface disclosure: all captured/failed surfaces rendered correctly — match
|
||||
- Material support: every material finding re-derived from raw capture — hold
|
||||
- Needs-verification open: 0
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"audit_engine": "v1",
|
||||
"timestamp": "2026-08-15T13:57:09.286220+00:00",
|
||||
"business": "Golden Hills Family Dental",
|
||||
"summary": {
|
||||
"total_findings": 10,
|
||||
"severity_counts": {
|
||||
"immediate": 3,
|
||||
"high": 1,
|
||||
"medium": 3,
|
||||
"enhancement": 3
|
||||
},
|
||||
"sub_score_issues": {
|
||||
"Digital Identity": 3,
|
||||
"Google Business": 1,
|
||||
"Technical SEO": 2,
|
||||
"Reviews": 3,
|
||||
"Content": 1
|
||||
},
|
||||
"surfaces_ok": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"website": true
|
||||
},
|
||||
"surfaces_failed": {
|
||||
"bing_places": false
|
||||
},
|
||||
"rating": 5.0,
|
||||
"reviews": 283
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "nap_phone_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Phone number inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "(916) 939-0889",
|
||||
"apple": "+1 (916) 939-0889",
|
||||
"website": "6389012842"
|
||||
},
|
||||
"recommendation": "Standardize phone format across all directories. GBP is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "nap_address_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Address inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "1010 White Rock Rd #500, El Dorado Hills, CA 95762",
|
||||
"apple": "1010 White Rock Rd, Unit 500, El Dorado Hills, CA 95762"
|
||||
},
|
||||
"recommendation": "Standardize exact address format. GBP address is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "hours_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Hours mismatch on 5 day(s) \u2014 GBP is authoritative",
|
||||
"sub_score": "Google Business",
|
||||
"evidence": [
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "8 AM-1 PM",
|
||||
"apple_maps": "8:00 AM-1:00 PM"
|
||||
}
|
||||
],
|
||||
"recommendation": "Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."
|
||||
},
|
||||
{
|
||||
"id": "missing_jsonld_schema",
|
||||
"severity": "high",
|
||||
"title": "No JSON-LD schema found on website",
|
||||
"sub_score": "Technical SEO",
|
||||
"evidence": {
|
||||
"url": "http://www.goldenhillsfamilydental.net/",
|
||||
"has_jsonld": false
|
||||
},
|
||||
"recommendation": "Add LocalBusiness/BeautySalon JSON-LD schema. Critical for rich results and local ranking."
|
||||
},
|
||||
{
|
||||
"id": "review_count_delta",
|
||||
"severity": "medium",
|
||||
"title": "Review count varies by 242 across surfaces",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"google": 283,
|
||||
"apple": 41
|
||||
},
|
||||
"recommendation": "Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership."
|
||||
},
|
||||
{
|
||||
"id": "review_dates_unavailable",
|
||||
"severity": "medium",
|
||||
"title": "Cannot determine review recency \u2014 no dates available in samples",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sources_with_dates": []
|
||||
},
|
||||
"recommendation": "Review date data is not available from current surfaces. Monitor via GBP dashboard directly."
|
||||
},
|
||||
{
|
||||
"id": "category_fragmentation",
|
||||
"severity": "medium",
|
||||
"title": "Category varies across surfaces: cosmetic dentist, health care",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "cosmetic dentist",
|
||||
"apple": "health care"
|
||||
},
|
||||
"recommendation": "Align primary category across all directories. GBP primary category is the ranking driver."
|
||||
},
|
||||
{
|
||||
"id": "reviews_low_specificity",
|
||||
"severity": "enhancement",
|
||||
"title": "Only 0% of sample reviews mention specific services or practitioners",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
},
|
||||
"recommendation": "Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better."
|
||||
},
|
||||
{
|
||||
"id": "no_price_level",
|
||||
"severity": "enhancement",
|
||||
"title": "Price level not set on GBP or Apple Maps",
|
||||
"sub_score": "Content",
|
||||
"evidence": {},
|
||||
"recommendation": "Set price level ($\u2013$$$$) on GBP. Helps users self-select and improves relevance."
|
||||
},
|
||||
{
|
||||
"id": "missing_og_tags",
|
||||
"severity": "enhancement",
|
||||
"title": "No Open Graph tags on website",
|
||||
"sub_score": "Technical SEO",
|
||||
"evidence": {
|
||||
"url": "http://www.goldenhillsfamilydental.net/"
|
||||
},
|
||||
"recommendation": "Add OG tags for proper link preview on social media and messaging apps."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
# Audit Findings — Golden Hills Family Dental
|
||||
|
||||
**Generated:** 2026-08-15 13:57 UTC
|
||||
**Sources checked:** google_business_profile, apple_maps, website
|
||||
**Rating:** 5.0 | **Reviews:** 283
|
||||
|
||||
## Severity Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🟥 Immediate | 3 |
|
||||
| 🟧 High | 1 |
|
||||
| 🟨 Medium | 3 |
|
||||
| 🟩 Enhancement | 3 |
|
||||
| **Total** | **10** |
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding #1: Phone number inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize phone format across all directories. GBP is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "(916) 939-0889",
|
||||
"apple": "+1 (916) 939-0889",
|
||||
"website": "6389012842"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #2: Address inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize exact address format. GBP address is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "1010 White Rock Rd #500, El Dorado Hills, CA 95762",
|
||||
"apple": "1010 White Rock Rd, Unit 500, El Dorado Hills, CA 95762"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #3: Hours mismatch on 5 day(s) — GBP is authoritative
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Google Business
|
||||
- **Recommendation:** Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss.
|
||||
- **Evidence:** ```json
|
||||
[
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "8 AM-5 PM",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "8 AM-1 PM",
|
||||
"apple_maps": "8:00 AM-1:00 PM"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Finding #4: No JSON-LD schema found on website
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Technical SEO
|
||||
- **Recommendation:** Add LocalBusiness/BeautySalon JSON-LD schema. Critical for rich results and local ranking.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"url": "http://www.goldenhillsfamilydental.net/",
|
||||
"has_jsonld": false
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #5: Review count varies by 242 across surfaces
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": 283,
|
||||
"apple": 41
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #6: Cannot determine review recency — no dates available in samples
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Review date data is not available from current surfaces. Monitor via GBP dashboard directly.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sources_with_dates": []
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #7: Category varies across surfaces: cosmetic dentist, health care
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Align primary category across all directories. GBP primary category is the ranking driver.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "cosmetic dentist",
|
||||
"apple": "health care"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #8: Only 0% of sample reviews mention specific services or practitioners
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #9: Price level not set on GBP or Apple Maps
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Set price level ($–$$$$) on GBP. Helps users self-select and improves relevance.
|
||||
|
||||
### Finding #10: No Open Graph tags on website
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Technical SEO
|
||||
- **Recommendation:** Add OG tags for proper link preview on social media and messaging apps.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"url": "http://www.goldenhillsfamilydental.net/"
|
||||
}
|
||||
```
|
||||
|
||||
## Failed Surfaces
|
||||
|
||||
- ❌ `bing_places` — no data returned
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"audit": {
|
||||
"timestamp": "2026-08-15T13:57:08.033037+00:00",
|
||||
"tool": "multi_scraper_v1",
|
||||
"surfaces_checked": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": false,
|
||||
"website": true
|
||||
},
|
||||
"verification": {
|
||||
"phone_mismatch": {
|
||||
"primary": "(916) 939-0889",
|
||||
"conflicts": {
|
||||
"apple": "+1 (916) 939-0889"
|
||||
}
|
||||
},
|
||||
"website_mismatch": {
|
||||
"primary": "http://www.goldenhillsfamilydental.net/",
|
||||
"conflicts": {
|
||||
"apple": "http://www.goldenhillsfamilydental.net"
|
||||
}
|
||||
},
|
||||
"hours_mismatch": {
|
||||
"monday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"tuesday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"wednesday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"thursday": {
|
||||
"google": "8 AM-5 PM",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"friday": {
|
||||
"google": "8 AM-1 PM",
|
||||
"apple": "8:00 AM-1:00 PM"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"primary_source": "google_business_profile",
|
||||
"name": "Golden Hills Family Dental",
|
||||
"address": "1010 White Rock Rd #500, El Dorado Hills, CA 95762",
|
||||
"phone": "(916) 939-0889",
|
||||
"website": "http://www.goldenhillsfamilydental.net/",
|
||||
"rating": 5.0,
|
||||
"reviews": 283,
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "We have been coming to Golden Hills Family Dental for 21 years and truly wouldn't go anywhere else. The entire staff is wonderful and makes every visit a great experience. They are the perfect balance",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "I had been seeing a dentist for a few years that I was not happy with them-so my sibling recommended Dr. Walls. She had been seeing him for years. I've been seeing Dr. Walls now for a few years-he exp",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "I absolutely LOVE going to the dentist!!!!! I know I sound strange, but I was a Dental Assistant for 23 years. so I know a little about dentistry. Dr Walls is phenomenal!!!!! He and his staff are extr",
|
||||
"rating": 5
|
||||
}
|
||||
],
|
||||
"hours": {
|
||||
"saturday": "Closed",
|
||||
"sunday": "Closed",
|
||||
"monday": "8 AM-5 PM",
|
||||
"tuesday": "8 AM-5 PM",
|
||||
"wednesday": "8 AM-5 PM",
|
||||
"thursday": "8 AM-5 PM",
|
||||
"friday": "8 AM-1 PM"
|
||||
},
|
||||
"category": "Cosmetic dentist",
|
||||
"price_level": null,
|
||||
"photos_count": null,
|
||||
"description": null,
|
||||
"coordinates": {
|
||||
"lat": 38.6468971,
|
||||
"lon": -121.0666979
|
||||
},
|
||||
"closed_status": {
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"sources": {
|
||||
"google_business_profile": {
|
||||
"name": "Golden Hills Family Dental",
|
||||
"address": "1010 White Rock Rd #500, El Dorado Hills, CA 95762",
|
||||
"phone": "(916) 939-0889",
|
||||
"website": "http://www.goldenhillsfamilydental.net/",
|
||||
"rating": 5.0,
|
||||
"reviews": 283,
|
||||
"hours": {
|
||||
"saturday": "Closed",
|
||||
"sunday": "Closed",
|
||||
"monday": "8 AM-5 PM",
|
||||
"tuesday": "8 AM-5 PM",
|
||||
"wednesday": "8 AM-5 PM",
|
||||
"thursday": "8 AM-5 PM",
|
||||
"friday": "8 AM-1 PM"
|
||||
},
|
||||
"category": "Cosmetic dentist",
|
||||
"price_level": null,
|
||||
"description": null,
|
||||
"photos_count": null,
|
||||
"latitude": 38.6468971,
|
||||
"longitude": -121.0666979,
|
||||
"url": "https://www.google.com/maps/place/Golden+Hills+Family+Dental/@38.6468971,-121.0666979,17z/data=!3m1!4b1!4m6!3m5!1s0x809afac8446a5e7b:0x696ab9d40f54be9c!8m2!3d38.6468971!4d-121.0666979!16s%2Fg%2F1vntk4cb?entry=ttu&g_ep=EgoyMDI2MDgxMi4wIKXMDSoASAFQAw%3D%3D",
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"apple_maps": {
|
||||
"address": "1010 White Rock Rd, Unit 500, El Dorado Hills, CA 95762",
|
||||
"hours": {
|
||||
"monday": "8:00 AM-5:00 PM",
|
||||
"tuesday": "8:00 AM-5:00 PM",
|
||||
"wednesday": "8:00 AM-5:00 PM",
|
||||
"thursday": "8:00 AM-5:00 PM",
|
||||
"friday": "8:00 AM-1:00 PM"
|
||||
},
|
||||
"rating": 4.7,
|
||||
"reviews": 41,
|
||||
"coordinates": {
|
||||
"lat": 38.6469106,
|
||||
"lon": -121.0664753
|
||||
},
|
||||
"name": "Golden Hills Family Dental",
|
||||
"category": "Health Care",
|
||||
"phone": "+1 (916) 939-0889",
|
||||
"website": "http://www.goldenhillsfamilydental.net",
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "We have been coming to Golden Hills Family Dental for 21 years and truly wouldn't go anywhere else. The entire staff is wonderful and makes every visit a great experience. They are the perfect balance",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "I had been seeing a dentist for a few years that I was not happy with them-so my sibling recommended Dr. Walls. She had been seeing him for years. I've been seeing Dr. Walls now for a few years-he exp",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "I absolutely LOVE going to the dentist!!!!! I know I sound strange, but I was a Dental Assistant for 23 years. so I know a little about dentistry. Dr Walls is phenomenal!!!!! He and his staff are extr",
|
||||
"rating": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"bing_places": null,
|
||||
"website": {
|
||||
"source_url": "http://www.goldenhillsfamilydental.net/",
|
||||
"title": "Cosmetic and family dentist, Dr. Steven Walls, uses CEREC, Invisalign, Implants -El Dorado Hills",
|
||||
"description": "Cosmetic, implants, veneers, botox, dentistry, crowns, caps, family dentist, El Dorado Hills, orthodontist",
|
||||
"phone_on_page": "6389012842"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
# Gordon Law
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** El Dorado Hills, CA
|
||||
**Vertical:** Law
|
||||
**Audit date:** 15 August 2026
|
||||
**Prepared for:** Gordon Law
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited Gordon Law across its primary online surfaces: Google Business Profile and Apple Maps. The business shows 4.9 ★ across 45 Google reviews.
|
||||
**4 material integrity issues were identified:**
|
||||
|
||||
1. Phone numbers are inconsistent across directories.
|
||||
2. Business hours are inconsistent between Google and Apple.
|
||||
3. No website could be located for this business.
|
||||
4. No business description is set on any directory.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✓ | 4.9 ★ | 45 |
|
||||
| Apple Maps | ✓ | No rating | No reviews |
|
||||
| Bing Places | ✗ | capture blocked (bot-wall) — excluded, not verified | — |
|
||||
| Website | ✗ | not found / unreachable at audit time | — |
|
||||
|
||||
> **Limitation:** The website could not be reached or located at audit time. A blocked or missing website is itself a customer-path risk: visitors arriving from search cannot engage the business. Reachability should be re-verified; if the block persists, treat it as a material finding in the next run.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Phone number inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists **(916) 354-6261**. Apple lists **+1 (916) 877-7822**.
|
||||
|
||||
**Impact:**
|
||||
Customers reaching the business through a single directory may call a disconnected or wrong number. Inconsistent NAP (name-address-phone) signals reduce local search ranking across all surfaces.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the correct primary number, then update every directory to match. Keep the verified number identical across Google, Apple, and the website.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: Hours mismatch between Google and Apple
|
||||
|
||||
**Observation:**
|
||||
monday: Google “Open 24 hours” vs Apple “8:00 AM-5:00 PM”; tuesday: Google “Open 24 hours” vs Apple “8:00 AM-5:00 PM”; wednesday: Google “Open 24 hours” vs Apple “8:00 AM-5:00 PM”; thursday: Google “Open 24 hours” vs Apple “8:00 AM-5:00 PM”; friday: Google “Open 24 hours” vs Apple “8:00 AM-5:00 PM”
|
||||
|
||||
**Impact:**
|
||||
Customers planning a visit see different availability depending on which directory they use. Mismatched hours drive no-shows and lost calls at the edge of the workday.
|
||||
|
||||
**Recommendation:**
|
||||
Verify actual operating hours with the business. Update the non-authoritative surface to match Google's verified schedule, or correct Google if its hours are inaccurate.
|
||||
|
||||
---
|
||||
|
||||
### Finding 3: Website listed on GBP but unreachable at audit time
|
||||
|
||||
**Observation:**
|
||||
GBP website='http://www.cgordonlaw.com/' — surface blocked; documented limitation per process §4
|
||||
|
||||
**Impact:**
|
||||
Without a website, the business cedes its owned property: no service pages, no schema, no conversion path beyond a phone call. AI search surfaces increasingly require a website to cite.
|
||||
|
||||
**Recommendation:**
|
||||
Stand up (or link on GBP) a business website with service pages, contact details, and LocalBusiness schema.
|
||||
|
||||
---
|
||||
|
||||
### Finding 4: No business description found on any surface
|
||||
|
||||
**Observation:**
|
||||
no description on any checked surface — weakens local-pack snippet
|
||||
|
||||
**Impact:**
|
||||
An empty description leaves the local-pack snippet to algorithmic default and forfeits the business's own service language in search results.
|
||||
|
||||
**Recommendation:**
|
||||
Write a 2-3 sentence GBP description covering services, service area, and differentiators.
|
||||
|
||||
---
|
||||
|
||||
## Verification Pending
|
||||
|
||||
- **Address inconsistent across 1 surface(s)** — unit designators differ: google='FL 2' vs apple='STEE 210' — verify on-site which is correct
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; ratings and counts only.
|
||||
- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*
|
||||
@@ -0,0 +1,50 @@
|
||||
# Validation Run: Gordon Law
|
||||
|
||||
**Date:** 15 August 2026
|
||||
**Business:** Gordon Law
|
||||
**Rating:** 4.9 ★ | **Reviews:** 45
|
||||
**Engine findings:** 8 | **Material (in report):** 4 | **Rejected FP/low-signal:** 2 | **Needs verification:** 1
|
||||
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
### Material (survive normalization → in REPORT-final.md)
|
||||
|
||||
- **immediate** — Phone number inconsistent across 1 surface(s)
|
||||
`distinct numbers: 9163546261, 9168777822`
|
||||
- **immediate** — Hours mismatch on 5 day(s) — GBP is authoritative
|
||||
`5 day(s) genuinely differ: monday: Open 24 hours vs 8:00 AM-5:00 PM; tuesday: Open 24 hours vs 8:00 AM-5:00 PM; wednesday: Open 24 hours vs 8:00 AM-5:00 PM; thursday: Open 24 hours vs 8:00 AM-5:00 PM`
|
||||
- **high** — Website listed on GBP but unreachable at audit time
|
||||
`GBP website='http://www.cgordonlaw.com/' — surface blocked; documented limitation per process §4`
|
||||
- **medium** — No business description found on any surface
|
||||
`no description on any checked surface — weakens local-pack snippet`
|
||||
|
||||
### Needs verification (excluded from report until confirmed)
|
||||
|
||||
- **immediate** — Address inconsistent across 1 surface(s)
|
||||
`unit designators differ: google='FL 2' vs apple='STEE 210' — verify on-site which is correct`
|
||||
|
||||
### Rejected (format false-positives / low-signal / tooling gaps)
|
||||
|
||||
- ~~Cannot determine review recency — no dates available in samples~~ — tooling gap: scraper captures no review dates (documented limitation)
|
||||
- ~~Category varies across surfaces: consumer sector, personal injury attorney~~ — low-signal: per-platform taxonomy (Apple 'consumer sector' is its default)
|
||||
|
||||
### Enhancement tier (noted, excluded from client report)
|
||||
|
||||
- Price level not set on GBP or Apple Maps
|
||||
|
||||
## Pre-delivery consistency gate (v1.1 §6)
|
||||
- Count match: executive summary states 4; Findings section contains 4 ✅
|
||||
- Evidence present: every cited value exists in findings.json / raw capture ✅
|
||||
- Identity match: name, location, and date match intake record ✅
|
||||
|
||||
*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*
|
||||
|
||||
## GATE RESULT
|
||||
|
||||
**PASS** — report_gate.py, 2026-08-15T16:27:11
|
||||
|
||||
- Count: 4 stated / 4 material after review — match
|
||||
- Identity: report title + date match raw capture ('Gordon Law', 15 August 2026) — match
|
||||
- Surface disclosure: all captured/failed surfaces rendered correctly — match
|
||||
- Material support: every material finding re-derived from raw capture — hold
|
||||
- Needs-verification open: 1
|
||||
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"audit_engine": "v1",
|
||||
"timestamp": "2026-08-15T13:53:45.012831+00:00",
|
||||
"business": "Gordon Law",
|
||||
"summary": {
|
||||
"total_findings": 8,
|
||||
"severity_counts": {
|
||||
"immediate": 3,
|
||||
"high": 1,
|
||||
"medium": 3,
|
||||
"enhancement": 1
|
||||
},
|
||||
"sub_score_issues": {
|
||||
"Digital Identity": 3,
|
||||
"Google Business": 1,
|
||||
"Technical SEO": 1,
|
||||
"Reviews": 1,
|
||||
"Content": 2
|
||||
},
|
||||
"surfaces_ok": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true
|
||||
},
|
||||
"surfaces_failed": {
|
||||
"bing_places": false,
|
||||
"website": false
|
||||
},
|
||||
"rating": 4.9,
|
||||
"reviews": 45
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "nap_phone_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Phone number inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "(916) 354-6261",
|
||||
"apple": "+1 (916) 877-7822"
|
||||
},
|
||||
"recommendation": "Standardize phone format across all directories. GBP is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "nap_address_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Address inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "5180 Golden Foothill Pkwy FL 2, El Dorado Hills, CA 95762",
|
||||
"apple": "5180 Golden Foothill Pkwy, Ste 210, El Dorado Hills, CA 95762"
|
||||
},
|
||||
"recommendation": "Standardize exact address format. GBP address is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "hours_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Hours mismatch on 5 day(s) \u2014 GBP is authoritative",
|
||||
"sub_score": "Google Business",
|
||||
"evidence": [
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
}
|
||||
],
|
||||
"recommendation": "Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."
|
||||
},
|
||||
{
|
||||
"id": "no_website",
|
||||
"severity": "high",
|
||||
"title": "No website found",
|
||||
"sub_score": "Technical SEO",
|
||||
"evidence": {},
|
||||
"recommendation": "A website is essential for credibility, schema markup, and conversion. Launch one immediately."
|
||||
},
|
||||
{
|
||||
"id": "review_dates_unavailable",
|
||||
"severity": "medium",
|
||||
"title": "Cannot determine review recency \u2014 no dates available in samples",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sources_with_dates": []
|
||||
},
|
||||
"recommendation": "Review date data is not available from current surfaces. Monitor via GBP dashboard directly."
|
||||
},
|
||||
{
|
||||
"id": "category_fragmentation",
|
||||
"severity": "medium",
|
||||
"title": "Category varies across surfaces: consumer sector, personal injury attorney",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "personal injury attorney",
|
||||
"apple": "consumer sector"
|
||||
},
|
||||
"recommendation": "Align primary category across all directories. GBP primary category is the ranking driver."
|
||||
},
|
||||
{
|
||||
"id": "no_description",
|
||||
"severity": "medium",
|
||||
"title": "No business description found on any surface",
|
||||
"sub_score": "Content",
|
||||
"evidence": {
|
||||
"surfaces_checked": [
|
||||
"google",
|
||||
"bing",
|
||||
"website"
|
||||
]
|
||||
},
|
||||
"recommendation": "Add a keyword-rich description to GBP and website. Descriptions improve click-through and category relevance."
|
||||
},
|
||||
{
|
||||
"id": "no_price_level",
|
||||
"severity": "enhancement",
|
||||
"title": "Price level not set on GBP or Apple Maps",
|
||||
"sub_score": "Content",
|
||||
"evidence": {},
|
||||
"recommendation": "Set price level ($\u2013$$$$) on GBP. Helps users self-select and improves relevance."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
# Audit Findings — Gordon Law
|
||||
|
||||
**Generated:** 2026-08-15 13:53 UTC
|
||||
**Sources checked:** google_business_profile, apple_maps
|
||||
**Rating:** 4.9 | **Reviews:** 45
|
||||
|
||||
## Severity Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🟥 Immediate | 3 |
|
||||
| 🟧 High | 1 |
|
||||
| 🟨 Medium | 3 |
|
||||
| 🟩 Enhancement | 1 |
|
||||
| **Total** | **8** |
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding #1: Phone number inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize phone format across all directories. GBP is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "(916) 354-6261",
|
||||
"apple": "+1 (916) 877-7822"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #2: Address inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize exact address format. GBP address is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "5180 Golden Foothill Pkwy FL 2, El Dorado Hills, CA 95762",
|
||||
"apple": "5180 Golden Foothill Pkwy, Ste 210, El Dorado Hills, CA 95762"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #3: Hours mismatch on 5 day(s) — GBP is authoritative
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Google Business
|
||||
- **Recommendation:** Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss.
|
||||
- **Evidence:** ```json
|
||||
[
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "Open 24 hours",
|
||||
"apple_maps": "8:00 AM-5:00 PM"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Finding #4: No website found
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Technical SEO
|
||||
- **Recommendation:** A website is essential for credibility, schema markup, and conversion. Launch one immediately.
|
||||
|
||||
### Finding #5: Cannot determine review recency — no dates available in samples
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Review date data is not available from current surfaces. Monitor via GBP dashboard directly.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sources_with_dates": []
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #6: Category varies across surfaces: consumer sector, personal injury attorney
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Align primary category across all directories. GBP primary category is the ranking driver.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "personal injury attorney",
|
||||
"apple": "consumer sector"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #7: No business description found on any surface
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Add a keyword-rich description to GBP and website. Descriptions improve click-through and category relevance.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"surfaces_checked": [
|
||||
"google",
|
||||
"bing",
|
||||
"website"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #8: Price level not set on GBP or Apple Maps
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Set price level ($–$$$$) on GBP. Helps users self-select and improves relevance.
|
||||
|
||||
## Failed Surfaces
|
||||
|
||||
- ❌ `bing_places` — no data returned
|
||||
- ❌ `website` — no data returned
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"audit": {
|
||||
"timestamp": "2026-08-15T13:53:44.844483+00:00",
|
||||
"tool": "multi_scraper_v1",
|
||||
"surfaces_checked": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": false,
|
||||
"website": false
|
||||
},
|
||||
"verification": {
|
||||
"name_mismatch": {
|
||||
"primary": "Gordon Law",
|
||||
"conflicts": {
|
||||
"apple": "The Gordon Law Firm"
|
||||
}
|
||||
},
|
||||
"phone_mismatch": {
|
||||
"primary": "(916) 354-6261",
|
||||
"conflicts": {
|
||||
"apple": "+1 (916) 877-7822"
|
||||
}
|
||||
},
|
||||
"website_mismatch": {
|
||||
"primary": "http://www.cgordonlaw.com/",
|
||||
"conflicts": {
|
||||
"apple": "http://www.cgordonlaw.com"
|
||||
}
|
||||
},
|
||||
"hours_mismatch": {
|
||||
"monday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"tuesday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"wednesday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"thursday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"friday": {
|
||||
"google": "Open 24 hours",
|
||||
"apple": "8:00 AM-5:00 PM"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"primary_source": "google_business_profile",
|
||||
"name": "Gordon Law",
|
||||
"address": "5180 Golden Foothill Pkwy FL 2, El Dorado Hills, CA 95762",
|
||||
"phone": "(916) 354-6261",
|
||||
"website": "http://www.cgordonlaw.com/",
|
||||
"rating": 4.9,
|
||||
"reviews": 45,
|
||||
"reviews_sample": null,
|
||||
"hours": {
|
||||
"saturday": "Open 24 hours",
|
||||
"sunday": "Open 24 hours",
|
||||
"monday": "Open 24 hours",
|
||||
"tuesday": "Open 24 hours",
|
||||
"wednesday": "Open 24 hours",
|
||||
"thursday": "Open 24 hours",
|
||||
"friday": "Open 24 hours"
|
||||
},
|
||||
"category": "Personal injury attorney",
|
||||
"price_level": null,
|
||||
"photos_count": null,
|
||||
"description": null,
|
||||
"coordinates": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
},
|
||||
"closed_status": {
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"sources": {
|
||||
"google_business_profile": {
|
||||
"name": "Gordon Law",
|
||||
"address": "5180 Golden Foothill Pkwy FL 2, El Dorado Hills, CA 95762",
|
||||
"phone": "(916) 354-6261",
|
||||
"website": "http://www.cgordonlaw.com/",
|
||||
"rating": 4.9,
|
||||
"reviews": 45,
|
||||
"hours": {
|
||||
"saturday": "Open 24 hours",
|
||||
"sunday": "Open 24 hours",
|
||||
"monday": "Open 24 hours",
|
||||
"tuesday": "Open 24 hours",
|
||||
"wednesday": "Open 24 hours",
|
||||
"thursday": "Open 24 hours",
|
||||
"friday": "Open 24 hours"
|
||||
},
|
||||
"category": "Personal injury attorney",
|
||||
"price_level": null,
|
||||
"description": null,
|
||||
"photos_count": null,
|
||||
"latitude": null,
|
||||
"longitude": null,
|
||||
"url": "https://www.google.com/maps/search/The%20Gordon%20Law%20Firm%20El%20Dorado%20Hills%2C%20CA/data=!3m1!4b1",
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"apple_maps": {
|
||||
"address": "5180 Golden Foothill Pkwy, Ste 210, El Dorado Hills, CA 95762",
|
||||
"hours": {
|
||||
"monday": "8:00 AM-5:00 PM",
|
||||
"tuesday": "8:00 AM-5:00 PM",
|
||||
"wednesday": "8:00 AM-5:00 PM",
|
||||
"thursday": "8:00 AM-5:00 PM",
|
||||
"friday": "8:00 AM-5:00 PM"
|
||||
},
|
||||
"coordinates": {
|
||||
"lat": 38.6394443,
|
||||
"lon": -121.0668444
|
||||
},
|
||||
"name": "The Gordon Law Firm",
|
||||
"category": "Consumer Sector",
|
||||
"phone": "+1 (916) 877-7822",
|
||||
"website": "http://www.cgordonlaw.com"
|
||||
},
|
||||
"bing_places": null,
|
||||
"website": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
# Scotty's Heating & Air
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** Placerville, CA
|
||||
**Vertical:** HVAC
|
||||
**Audit date:** 15 August 2026
|
||||
**Prepared for:** Scotty's Heating & Air
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited Scotty's Heating & Air across its primary online surfaces: Google Business Profile, Apple Maps and Website. The business shows 4.8 ★ across 180 Google reviews.
|
||||
**3 material integrity issues were identified:**
|
||||
|
||||
1. Phone numbers are inconsistent across directories.
|
||||
2. The website URL does not align across directories.
|
||||
3. Ratings vary across surfaces.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✓ | 4.8 ★ | 180 |
|
||||
| Apple Maps | ✓ | 4.1 ★ | 37 |
|
||||
| Bing Places | ✗ | capture blocked (bot-wall) — excluded, not verified | — |
|
||||
| Website | ✓ | — | — |
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: Phone number inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists **(530) 626-6000**. Apple lists **+1 (530) 626-6000**. Website lists **530-429-7616**.
|
||||
|
||||
**Impact:**
|
||||
Customers reaching the business through a single directory may call a disconnected or wrong number. Inconsistent NAP (name-address-phone) signals reduce local search ranking across all surfaces.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the correct primary number, then update every directory to match. Keep the verified number identical across Google, Apple, and the website.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: Website URL carries UTM parameters on one listing
|
||||
|
||||
**Observation:**
|
||||
google='https://www.scottysheatingandair.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp' vs apple='https://www.scottysheatingandair.com/' — same domain, attribution/SEO split risk (Gilmore precedent: material)
|
||||
|
||||
**Impact:**
|
||||
Divergent website URLs split SEO equity and distort analytics attribution; link signals may not consolidate on the canonical domain.
|
||||
|
||||
**Recommendation:**
|
||||
Point every directory at the canonical website URL (https, no tracking parameters) and consolidate redirects so link equity consolidates on one domain.
|
||||
|
||||
---
|
||||
|
||||
### Finding 3: Rating varies by 0.7 across surfaces (range: 4.1–4.8)
|
||||
|
||||
**Observation:**
|
||||
Google shows **4.8** ★ while Apple Maps shows **4.1** ★.
|
||||
|
||||
**Impact:**
|
||||
A rating spread across surfaces undermines the consistency signal local search relies on and can suppress the lower-rated surface in results.
|
||||
|
||||
**Recommendation:**
|
||||
Verify which rating is current; the stale surface usually reflects an unmanaged listing that has stopped receiving reviews.
|
||||
|
||||
---
|
||||
|
||||
## Verification Pending
|
||||
|
||||
- **Hours mismatch on 5 day(s) — GBP is authoritative** — 5 day(s) affected by '1800' parse artifact: monday: 8 AM-4:30 PM vs 8:00 AM-4:1800 PM; tuesday: 8 AM-4:30 PM vs 8:00 AM-4:1800 PM; wednesday: 8 AM-4:30 PM vs 8:00 AM-4:1800 PM — verify against live Apple Maps; excluded from report until confirmed
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; ratings and counts only.
|
||||
- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*
|
||||
@@ -0,0 +1,51 @@
|
||||
# Validation Run: Scotty's Heating & Air
|
||||
|
||||
**Date:** 15 August 2026
|
||||
**Business:** Scotty's Heating & Air
|
||||
**Rating:** 4.8 ★ | **Reviews:** 180
|
||||
**Engine findings:** 10 | **Material (in report):** 3 | **Rejected FP/low-signal:** 4 | **Needs verification:** 1
|
||||
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
### Material (survive normalization → in REPORT-final.md)
|
||||
|
||||
- **immediate** — Phone number inconsistent across 1 surface(s)
|
||||
`distinct numbers: 5304297616, 5306266000`
|
||||
- **medium** — Website URL carries UTM parameters on one listing
|
||||
`google='https://www.scottysheatingandair.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp' vs apple='https://www.scottysheatingandair.com/' — same domain, attribution/SEO split risk (Gilmore precedent: material)`
|
||||
- **high** — Rating varies by 0.7 across surfaces (range: 4.1–4.8)
|
||||
`google 4.8 vs apple 4.1`
|
||||
|
||||
### Needs verification (excluded from report until confirmed)
|
||||
|
||||
- **immediate** — Hours mismatch on 5 day(s) — GBP is authoritative
|
||||
`5 day(s) affected by '1800' parse artifact: monday: 8 AM-4:30 PM vs 8:00 AM-4:1800 PM; tuesday: 8 AM-4:30 PM vs 8:00 AM-4:1800 PM; wednesday: 8 AM-4:30 PM vs 8:00 AM-4:1800 PM — verify against live Apple Maps; excluded from report until confirmed`
|
||||
|
||||
### Rejected (format false-positives / low-signal / tooling gaps)
|
||||
|
||||
- ~~Address inconsistent across 1 surface(s)~~ — same street/city/zip; suite format only (# vs Ste vs Unit)
|
||||
- ~~Review count varies by 143 across surfaces~~ — low-signal: platforms count reviews differently; not an integrity issue
|
||||
- ~~Cannot determine review recency — no dates available in samples~~ — tooling gap: scraper captures no review dates (documented limitation)
|
||||
- ~~Category varies across surfaces: consumer sector, hvac contractor~~ — low-signal: per-platform taxonomy (Apple 'consumer sector' is its default)
|
||||
|
||||
### Enhancement tier (noted, excluded from client report)
|
||||
|
||||
- Only 0% of sample reviews mention specific services or practitioners
|
||||
- Price level not set on GBP or Apple Maps
|
||||
|
||||
## Pre-delivery consistency gate (v1.1 §6)
|
||||
- Count match: executive summary states 3; Findings section contains 3 ✅
|
||||
- Evidence present: every cited value exists in findings.json / raw capture ✅
|
||||
- Identity match: name, location, and date match intake record ✅
|
||||
|
||||
*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*
|
||||
|
||||
## GATE RESULT
|
||||
|
||||
**PASS** — report_gate.py, 2026-08-15T16:27:11
|
||||
|
||||
- Count: 3 stated / 3 material after review — match
|
||||
- Identity: report title + date match raw capture ("Scotty's Heating & Air", 15 August 2026) — match
|
||||
- Surface disclosure: all captured/failed surfaces rendered correctly — match
|
||||
- Material support: every material finding re-derived from raw capture — hold
|
||||
- Needs-verification open: 1
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"audit_engine": "v1",
|
||||
"timestamp": "2026-08-15T13:55:25.553652+00:00",
|
||||
"business": "Scotty's Heating & Air",
|
||||
"summary": {
|
||||
"total_findings": 10,
|
||||
"severity_counts": {
|
||||
"immediate": 3,
|
||||
"high": 2,
|
||||
"medium": 3,
|
||||
"enhancement": 2
|
||||
},
|
||||
"sub_score_issues": {
|
||||
"Digital Identity": 4,
|
||||
"Google Business": 1,
|
||||
"Reviews": 4,
|
||||
"Content": 1
|
||||
},
|
||||
"surfaces_ok": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"website": true
|
||||
},
|
||||
"surfaces_failed": {
|
||||
"bing_places": false
|
||||
},
|
||||
"rating": 4.8,
|
||||
"reviews": 180
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "nap_phone_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Phone number inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "(530) 626-6000",
|
||||
"apple": "+1 (530) 626-6000",
|
||||
"website": "530-429-7616"
|
||||
},
|
||||
"recommendation": "Standardize phone format across all directories. GBP is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "nap_address_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Address inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "3655 Chuckwagon Way Ste b, Placerville, CA 95667",
|
||||
"apple": "3655 Chuckwagon Way, Placerville, CA 95667"
|
||||
},
|
||||
"recommendation": "Standardize exact address format. GBP address is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "hours_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Hours mismatch on 5 day(s) \u2014 GBP is authoritative",
|
||||
"sub_score": "Google Business",
|
||||
"evidence": [
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple_maps": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple_maps": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple_maps": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple_maps": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple_maps": "8:00 AM-4:1800 PM"
|
||||
}
|
||||
],
|
||||
"recommendation": "Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."
|
||||
},
|
||||
{
|
||||
"id": "nap_website_mismatch",
|
||||
"severity": "high",
|
||||
"title": "Website URL inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "https://www.scottysheatingandair.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"apple": "https://www.scottysheatingandair.com/"
|
||||
},
|
||||
"recommendation": "Use one canonical URL (prefer HTTPS, no www) across all directories."
|
||||
},
|
||||
{
|
||||
"id": "rating_delta_high",
|
||||
"severity": "high",
|
||||
"title": "Rating varies by 0.7 across surfaces (range: 4.1\u20134.8)",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"google": 4.8,
|
||||
"apple": 4.1
|
||||
},
|
||||
"recommendation": "Investigate why some surfaces show different ratings. May indicate stale data or mixed-up business listings."
|
||||
},
|
||||
{
|
||||
"id": "review_count_delta",
|
||||
"severity": "medium",
|
||||
"title": "Review count varies by 143 across surfaces",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"google": 180,
|
||||
"apple": 37
|
||||
},
|
||||
"recommendation": "Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership."
|
||||
},
|
||||
{
|
||||
"id": "review_dates_unavailable",
|
||||
"severity": "medium",
|
||||
"title": "Cannot determine review recency \u2014 no dates available in samples",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sources_with_dates": []
|
||||
},
|
||||
"recommendation": "Review date data is not available from current surfaces. Monitor via GBP dashboard directly."
|
||||
},
|
||||
{
|
||||
"id": "category_fragmentation",
|
||||
"severity": "medium",
|
||||
"title": "Category varies across surfaces: consumer sector, hvac contractor",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "hvac contractor",
|
||||
"apple": "consumer sector"
|
||||
},
|
||||
"recommendation": "Align primary category across all directories. GBP primary category is the ranking driver."
|
||||
},
|
||||
{
|
||||
"id": "reviews_low_specificity",
|
||||
"severity": "enhancement",
|
||||
"title": "Only 0% of sample reviews mention specific services or practitioners",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
},
|
||||
"recommendation": "Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better."
|
||||
},
|
||||
{
|
||||
"id": "no_price_level",
|
||||
"severity": "enhancement",
|
||||
"title": "Price level not set on GBP or Apple Maps",
|
||||
"sub_score": "Content",
|
||||
"evidence": {},
|
||||
"recommendation": "Set price level ($\u2013$$$$) on GBP. Helps users self-select and improves relevance."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
# Audit Findings — Scotty's Heating & Air
|
||||
|
||||
**Generated:** 2026-08-15 13:55 UTC
|
||||
**Sources checked:** google_business_profile, apple_maps, website
|
||||
**Rating:** 4.8 | **Reviews:** 180
|
||||
|
||||
## Severity Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🟥 Immediate | 3 |
|
||||
| 🟧 High | 2 |
|
||||
| 🟨 Medium | 3 |
|
||||
| 🟩 Enhancement | 2 |
|
||||
| **Total** | **10** |
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding #1: Phone number inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize phone format across all directories. GBP is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "(530) 626-6000",
|
||||
"apple": "+1 (530) 626-6000",
|
||||
"website": "530-429-7616"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #2: Address inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize exact address format. GBP address is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "3655 Chuckwagon Way Ste b, Placerville, CA 95667",
|
||||
"apple": "3655 Chuckwagon Way, Placerville, CA 95667"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #3: Hours mismatch on 5 day(s) — GBP is authoritative
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Google Business
|
||||
- **Recommendation:** Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss.
|
||||
- **Evidence:** ```json
|
||||
[
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple_maps": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple_maps": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple_maps": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple_maps": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple_maps": "8:00 AM-4:1800 PM"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Finding #4: Website URL inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Use one canonical URL (prefer HTTPS, no www) across all directories.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "https://www.scottysheatingandair.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"apple": "https://www.scottysheatingandair.com/"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #5: Rating varies by 0.7 across surfaces (range: 4.1–4.8)
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Investigate why some surfaces show different ratings. May indicate stale data or mixed-up business listings.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": 4.8,
|
||||
"apple": 4.1
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #6: Review count varies by 143 across surfaces
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Large review count gaps suggest some surfaces have stale or merged data. Verify listing ownership.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": 180,
|
||||
"apple": 37
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #7: Cannot determine review recency — no dates available in samples
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Review date data is not available from current surfaces. Monitor via GBP dashboard directly.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sources_with_dates": []
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #8: Category varies across surfaces: consumer sector, hvac contractor
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Align primary category across all directories. GBP primary category is the ranking driver.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "hvac contractor",
|
||||
"apple": "consumer sector"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #9: Only 0% of sample reviews mention specific services or practitioners
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sample_size": 3,
|
||||
"specific_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #10: Price level not set on GBP or Apple Maps
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Set price level ($–$$$$) on GBP. Helps users self-select and improves relevance.
|
||||
|
||||
## Failed Surfaces
|
||||
|
||||
- ❌ `bing_places` — no data returned
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"audit": {
|
||||
"timestamp": "2026-08-15T13:55:25.119731+00:00",
|
||||
"tool": "multi_scraper_v1",
|
||||
"surfaces_checked": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": false,
|
||||
"website": true
|
||||
},
|
||||
"verification": {
|
||||
"phone_mismatch": {
|
||||
"primary": "(530) 626-6000",
|
||||
"conflicts": {
|
||||
"apple": "+1 (530) 626-6000"
|
||||
}
|
||||
},
|
||||
"website_mismatch": {
|
||||
"primary": "https://www.scottysheatingandair.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"conflicts": {
|
||||
"apple": "https://www.scottysheatingandair.com/"
|
||||
}
|
||||
},
|
||||
"hours_mismatch": {
|
||||
"monday": {
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
"tuesday": {
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
"wednesday": {
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
"thursday": {
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
"friday": {
|
||||
"google": "8 AM-4:30 PM",
|
||||
"apple": "8:00 AM-4:1800 PM"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"primary_source": "google_business_profile",
|
||||
"name": "Scotty's Heating & Air",
|
||||
"address": "3655 Chuckwagon Way Ste b, Placerville, CA 95667",
|
||||
"phone": "(530) 626-6000",
|
||||
"website": "https://www.scottysheatingandair.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"rating": 4.8,
|
||||
"reviews": 180,
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "My AC quit in the middle of a heatwave. Called Scotty's and got same day service. The technician, Steve was prompt and knowledgeable, pinpointing the problem immediately. He answered all my 'novice' A",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "I wish I could award negative stars. Don't expect Scotty to support you on any warranty issues. I had two bad experiences with him regarding warranties. One concerned the failure of the condensate pum",
|
||||
"rating": 1
|
||||
},
|
||||
{
|
||||
"text": "I have used Scottys for awhile. I recently had them install a whole house fan which they completed satisfactory. Tim has always done a great job.",
|
||||
"rating": 5
|
||||
}
|
||||
],
|
||||
"hours": {
|
||||
"saturday": "Closed",
|
||||
"sunday": "Closed",
|
||||
"monday": "8 AM-4:30 PM",
|
||||
"tuesday": "8 AM-4:30 PM",
|
||||
"wednesday": "8 AM-4:30 PM",
|
||||
"thursday": "8 AM-4:30 PM",
|
||||
"friday": "8 AM-4:30 PM"
|
||||
},
|
||||
"category": "HVAC contractor",
|
||||
"price_level": null,
|
||||
"photos_count": null,
|
||||
"description": null,
|
||||
"coordinates": {
|
||||
"lat": 38.6995472,
|
||||
"lon": -120.8176188
|
||||
},
|
||||
"closed_status": {
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"sources": {
|
||||
"google_business_profile": {
|
||||
"name": "Scotty's Heating & Air",
|
||||
"address": "3655 Chuckwagon Way Ste b, Placerville, CA 95667",
|
||||
"phone": "(530) 626-6000",
|
||||
"website": "https://www.scottysheatingandair.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"rating": 4.8,
|
||||
"reviews": 180,
|
||||
"hours": {
|
||||
"saturday": "Closed",
|
||||
"sunday": "Closed",
|
||||
"monday": "8 AM-4:30 PM",
|
||||
"tuesday": "8 AM-4:30 PM",
|
||||
"wednesday": "8 AM-4:30 PM",
|
||||
"thursday": "8 AM-4:30 PM",
|
||||
"friday": "8 AM-4:30 PM"
|
||||
},
|
||||
"category": "HVAC contractor",
|
||||
"price_level": null,
|
||||
"description": null,
|
||||
"photos_count": null,
|
||||
"latitude": 38.6995472,
|
||||
"longitude": -120.8176188,
|
||||
"url": "https://www.google.com/maps/place/Scotty's+Heating+%26+Air/@38.6995472,-120.8176188,17z/data=!3m1!4b1!4m6!3m5!1s0x809a4390373459a5:0x7097249e59073c9f!8m2!3d38.6995472!4d-120.8176188!16s%2Fg%2F1tf7bbp9?entry=ttu&g_ep=EgoyMDI2MDgxMi4wIKXMDSoASAFQAw%3D%3D",
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"apple_maps": {
|
||||
"address": "3655 Chuckwagon Way, Placerville, CA 95667",
|
||||
"hours": {
|
||||
"monday": "8:00 AM-4:1800 PM",
|
||||
"tuesday": "8:00 AM-4:1800 PM",
|
||||
"wednesday": "8:00 AM-4:1800 PM",
|
||||
"thursday": "8:00 AM-4:1800 PM",
|
||||
"friday": "8:00 AM-4:1800 PM"
|
||||
},
|
||||
"rating": 4.1,
|
||||
"reviews": 37,
|
||||
"coordinates": {
|
||||
"lat": 38.699576,
|
||||
"lon": -120.817519
|
||||
},
|
||||
"name": "Scotty's Heating & Air",
|
||||
"category": "Consumer Sector",
|
||||
"phone": "+1 (530) 626-6000",
|
||||
"website": "https://www.scottysheatingandair.com/",
|
||||
"reviews_sample": [
|
||||
{
|
||||
"text": "My AC quit in the middle of a heatwave. Called Scotty's and got same day service. The technician, Steve was prompt and knowledgeable, pinpointing the problem immediately. He answered all my 'novice' A",
|
||||
"rating": 5
|
||||
},
|
||||
{
|
||||
"text": "I wish I could award negative stars. Don't expect Scotty to support you on any warranty issues. I had two bad experiences with him regarding warranties. One concerned the failure of the condensate pum",
|
||||
"rating": 1
|
||||
},
|
||||
{
|
||||
"text": "I have used Scottys for awhile. I recently had them install a whole house fan which they completed satisfactory. Tim has always done a great job.",
|
||||
"rating": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"bing_places": null,
|
||||
"website": {
|
||||
"source_url": "https://www.scottysheatingandair.com/?utm_source=google&utm_medium=organic&utm_campaign=gbp",
|
||||
"title": "Scotty's Heating & Air - HVAC Services in Placerville, CA",
|
||||
"description": "Providing expert heating and air conditioning services in Placerville and surrounding areas since 2005. Your comfort is our priority.",
|
||||
"phone_on_page": "530-429-7616"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
# Appleseed Horticulture
|
||||
## Online Presence Audit
|
||||
|
||||
**Location:** 560 Placerville Dr, Placerville, CA 95667
|
||||
**Vertical:** Garden center / garden & grow supply
|
||||
**Audit date:** 16 August 2026
|
||||
**Prepared for:** Appleseed Horticulture
|
||||
**Prepared by:** VeriPath
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We audited Appleseed Horticulture across its primary online surfaces: Google Business Profile, Apple Maps, and the business's web property. The business is strong on trust — **4.9 ★ across 84 Google reviews** — and its name, address, and phone number are consistent across the major directories.
|
||||
|
||||
**4 material integrity issues were identified:**
|
||||
|
||||
1. No functioning website is linked on the business profile, and the business's own domain is offline.
|
||||
2. The business category is inconsistent between directories (garden center vs. shopping).
|
||||
3. No business description is set on the Google profile.
|
||||
4. Operating hours disagree between Google and Apple Maps on the weekend.
|
||||
|
||||
The profile itself is healthy and well-rated. The issues below are about the *owned web property* and *cross-directory consistency*, not about the business's reputation.
|
||||
|
||||
---
|
||||
|
||||
## Surfaces Reviewed
|
||||
|
||||
| Surface | Status | Rating | Reviews |
|
||||
|---------|--------|--------|---------|
|
||||
| Google Business Profile | ✓ | 4.9 ★ | 84 |
|
||||
| Apple Maps | ✓ | — (not surfaced) | — (not surfaced) |
|
||||
| Bing Places | ✗ | capture blocked (bot-wall) — excluded, not verified | — |
|
||||
| Website | ✓ | proxy capture only (Facebook page) — no owned site (see Finding 1) | — |
|
||||
|
||||
**Verified NAP (name-address-phone):**
|
||||
|
||||
| Field | Value | Sources agreeing |
|
||||
|-------|-------|------------------|
|
||||
| Name | Appleseed Horticulture | Google, Apple, Yelp, Facebook |
|
||||
| Address | 560 Placerville Dr, Placerville, CA 95667 | Google, Apple |
|
||||
| Phone | (530) 622-5190 | Google, Apple, Yelp, Facebook |
|
||||
|
||||
NAP is consistent across all four sources. No name, address, or phone conflicts were found.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding 1: No functioning website is linked to the business
|
||||
|
||||
**Observation:**
|
||||
The Google Business Profile has **no website field set**. The business's Facebook page lists `appleseedhorticulture.net`, but that domain does not resolve (DNS returns no record). A related domain, `appleseedhorticulture.com`, resolves but serves a dormant Shopify "Store unavailable" page — not a live business site.
|
||||
|
||||
**Impact:**
|
||||
Without a working website, the business has no owned property: no service or inventory pages, no structured data (schema), no online ordering or catalog, and no destination for the profile to point to. It also limits how the business appears in AI and map-based answers, which increasingly rely on a citable website. All web presence currently routes through third-party pages (Facebook, Yelp, directories) the business does not control.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm which domain is the intended one and bring it back online (or register a clean domain), then link it on the Google profile, Apple, and Facebook. At minimum the site should carry the address, phone, hours, a short service/inventory description, and a LocalBusiness schema block.
|
||||
|
||||
---
|
||||
|
||||
### Finding 2: Business category is inconsistent across directories
|
||||
|
||||
**Observation:**
|
||||
Google lists the primary category as **"Garden center."** Apple Maps lists it as **"Shopping."**
|
||||
|
||||
**Impact:**
|
||||
The category is a core local-ranking signal. Apple's generic "Shopping" bucket is far less specific than "Garden center," so the Apple listing is optimized for fewer, less relevant searches (e.g., "garden center Placerville," "garden supplies," "hydroponics"). The mismatch also weakens the consistency signal local search relies on.
|
||||
|
||||
**Recommendation:**
|
||||
Align the category to the most specific accurate one on every surface — "Garden center" (with "Nursery" / "Garden supply" as secondary categories where the platform allows). Confirm with the owner which categories best match the actual mix of plants, soil, and grow/hydroponic supplies sold.
|
||||
|
||||
---
|
||||
|
||||
### Finding 3: No business description is set on the Google profile
|
||||
|
||||
**Observation:**
|
||||
No business description is present on the Google profile, and no description was captured on any other checked surface.
|
||||
|
||||
**Impact:**
|
||||
An empty description leaves the local-pack snippet to Google's default rendering and forfeits the business's own language about what it sells (plants, soil, garden and grow/hydroponic supplies, family-owned since 2012). That's lost relevance for category searches and a weaker click-through in the map pack.
|
||||
|
||||
**Recommendation:**
|
||||
Write a 2–3 sentence Google profile description covering what the business sells, that it is family-owned, and the service area. Reuse the same language on Apple and Facebook so the description is consistent everywhere.
|
||||
|
||||
---
|
||||
|
||||
### Finding 4: Operating hours disagree between Google and Apple Maps (weekend)
|
||||
|
||||
**Observation:**
|
||||
Google (authoritative) lists **Sat 10 AM–5 PM** and **Sun 11 AM–4 PM**. Apple Maps lists **Sat 10:00 AM–4:00 PM** and **Sun 10:00 AM–4:00 PM** — so Saturday shows a one-hour earlier close and Sunday a one-hour earlier open on Apple. (Weekday Apple hours were captured with a formatting error and are excluded from this finding.)
|
||||
|
||||
**Impact:**
|
||||
Customers planning a visit see different weekend availability depending on which directory they use. Mismatched hours cause missed visits and confusion, especially at the edges of the weekend day.
|
||||
|
||||
**Recommendation:**
|
||||
Confirm the actual weekend hours with the owner, then set the authoritative schedule on Google and align Apple (and Facebook) to match exactly.
|
||||
|
||||
---
|
||||
|
||||
## Priority Actions
|
||||
|
||||
| # | Action | Effort | Impact |
|
||||
|---|--------|--------|--------|
|
||||
| 1 | Bring the business website back online and link it on all profiles | L | High |
|
||||
| 2 | Align category to "Garden center" across surfaces | S | Medium |
|
||||
| 3 | Write and set the business description | S | Medium |
|
||||
| 4 | Confirm weekend hours and align Apple to Google | S | Medium |
|
||||
|
||||
---
|
||||
|
||||
## Data Limitations
|
||||
|
||||
This audit ran without authenticated access to the business's listings. Known ceilings:
|
||||
|
||||
- **Review text and dates** were not captured; rating and count only (Google: 4.9 ★ / 84).
|
||||
- **Bing Places** is bot-walled for headless capture in this run; that surface is excluded from the audit, not verified.
|
||||
- **Apple Maps** did not surface a rating or review count on the listing; its hours capture contained a weekday formatting error (weekend deltas only were used).
|
||||
- **Photos** count on the Google profile was not captured in this run.
|
||||
- This is a **point-in-time snapshot**; directory data changes after the audit date.
|
||||
|
||||
*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies (phone country-code normalization, a weekday hours capture artifact) were normalized and excluded before inclusion. Phone number verified consistent across Google, Apple, Yelp, and Facebook.*
|
||||
@@ -0,0 +1,51 @@
|
||||
# Validation Run: Appleseed Horticulture
|
||||
|
||||
**Date:** 16 August 2026
|
||||
**Business:** Appleseed Horticulture
|
||||
**Rating:** 4.9 ★ | **Reviews:** 84
|
||||
**Engine findings:** 8 | **Material (in report):** 4 | **Rejected FP/low-signal:** 2 | **Needs verification:** 0
|
||||
|
||||
## Reviewer dispositions (locked process v1.1 §3)
|
||||
|
||||
### Material (in REPORT-final.md)
|
||||
|
||||
- **immediate** — Hours mismatch on 7 day(s) — GBP is authoritative
|
||||
`Approved, scoped: 5 of 7 deltas are a scraper artifact (Apple captured as "5:1800 PM" — timezone leak, PARSE1800). Genuinely differing days re-derived from raw capture: Sunday (11 AM–4 PM vs 10:00 AM–4:00 PM) and Saturday close (5 PM vs 4:00 PM). Report Finding 4 carries the clean weekend deltas only; the weekday capture artifact is documented in Data Limitations.`
|
||||
- **high** — No JSON-LD schema found on website
|
||||
`Approved, re-scoped: the "website" surface captured in this run is the business's Facebook page because GBP has no website field. The true claim is larger: no functioning owned website exists. FB lists appleseedhorticulture.net (NXDOMAIN — dead, verified by DNS 2026-08-16) and appleseedhorticulture.com resolves to a dormant Shopify "Store unavailable" page (verified by HTTP 2026-08-16). Report Finding 1.`
|
||||
- **medium** — No business description found on any surface
|
||||
`Approved: gbp description = null, apple description = null in raw capture. Report Finding 3.`
|
||||
|
||||
### Approved beyond engine materiality
|
||||
|
||||
- **medium** — Category varies across surfaces: garden center, shopping
|
||||
`Engine classified low-signal (per-platform taxonomy). Reviewer approves as material: Apple's generic "Shopping" bucket is a real discoverability loss for "garden center" category searches. Report Finding 2.`
|
||||
|
||||
### Rejected (false-positive / tooling gap)
|
||||
|
||||
- ~~Phone number inconsistent across 1 surface(s)~~ — `False positive: the "conflicting" third number 8666666666 is a Facebook page JS artifact captured in website.phone_on_page, not a business number. Google and Apple are identical after +1 normalization ((530) 622-5190). Phone independently corroborated consistent on Yelp and Facebook.`
|
||||
- ~~Cannot determine review recency — no dates available in samples~~ — `Tooling gap: scraper captures no review dates. Documented as a Data Limitation, not a finding.`
|
||||
|
||||
### Enhancement tier (noted, excluded from client report per v1.1)
|
||||
|
||||
- Price level not set on GBP or Apple Maps
|
||||
- No Open Graph tags on website (subsumed by Finding 1 — no owned site to tag)
|
||||
|
||||
## Blocked surfaces (v1.1 §4)
|
||||
|
||||
- **Bing Places** — no data returned (bot-wall). Noted in the Surfaces Reviewed table with the failure reason; excluded from the audit, not reported as a bare ✗.
|
||||
- **Website** — capture succeeded against the Facebook proxy page. The absence of an owned website is carried as Finding 1 (customer-path leakage), not as a blocked surface.
|
||||
|
||||
## Legacy check (conditional)
|
||||
|
||||
Not triggered — no name or location change in the last 3–5 years indicated at intake. (The dormant .com / dead .net domains are covered under Finding 1.)
|
||||
|
||||
## GATE RESULT
|
||||
|
||||
**PASS** — report_gate.py, 2026-08-16T01:20:15
|
||||
|
||||
- Count: 4 stated / 4 material after review — match
|
||||
- Identity: report title + date match raw capture ('Appleseed Horticulture', 16 August 2026) — match
|
||||
- Surface disclosure: all captured/failed surfaces rendered correctly — match
|
||||
- Material support: every material finding re-derived from raw capture — hold
|
||||
- Needs-verification open: 0
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"audit": {
|
||||
"timestamp": "2026-08-16T01:01:21.118029+00:00",
|
||||
"tool": "multi_scraper_v1",
|
||||
"surfaces_checked": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"bing_places": false,
|
||||
"website": true
|
||||
},
|
||||
"verification": {
|
||||
"phone_mismatch": {
|
||||
"primary": "(530) 622-5190",
|
||||
"conflicts": {
|
||||
"apple": "+1 (530) 622-5190"
|
||||
}
|
||||
},
|
||||
"hours_mismatch": {
|
||||
"saturday": {
|
||||
"google": "10 AM-5 PM",
|
||||
"apple": "10:00 AM-4:00 PM"
|
||||
},
|
||||
"sunday": {
|
||||
"google": "11 AM-4 PM",
|
||||
"apple": "10:00 AM-4:00 PM"
|
||||
},
|
||||
"monday": {
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
"tuesday": {
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
"wednesday": {
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
"thursday": {
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
"friday": {
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple": "9:00 AM-5:1800 PM"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"primary_source": "google_business_profile",
|
||||
"name": "Appleseed Horticulture",
|
||||
"address": "560 Placerville Dr, Placerville, CA 95667",
|
||||
"phone": "(530) 622-5190",
|
||||
"website": null,
|
||||
"rating": 4.9,
|
||||
"reviews": 84,
|
||||
"reviews_sample": null,
|
||||
"hours": {
|
||||
"saturday": "10 AM-5 PM",
|
||||
"sunday": "11 AM-4 PM",
|
||||
"monday": "9:30 AM-5:30 PM",
|
||||
"tuesday": "9:30 AM-5:30 PM",
|
||||
"wednesday": "9:30 AM-5:30 PM",
|
||||
"thursday": "9:30 AM-5:30 PM",
|
||||
"friday": "9:30 AM-5:30 PM"
|
||||
},
|
||||
"category": "Garden center",
|
||||
"price_level": null,
|
||||
"photos_count": null,
|
||||
"description": null,
|
||||
"coordinates": {
|
||||
"lat": 38.7328427,
|
||||
"lon": -120.8263245
|
||||
},
|
||||
"closed_status": {
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"sources": {
|
||||
"google_business_profile": {
|
||||
"name": "Appleseed Horticulture",
|
||||
"address": "560 Placerville Dr, Placerville, CA 95667",
|
||||
"phone": "(530) 622-5190",
|
||||
"website": null,
|
||||
"rating": 4.9,
|
||||
"reviews": 84,
|
||||
"hours": {
|
||||
"saturday": "10 AM-5 PM",
|
||||
"sunday": "11 AM-4 PM",
|
||||
"monday": "9:30 AM-5:30 PM",
|
||||
"tuesday": "9:30 AM-5:30 PM",
|
||||
"wednesday": "9:30 AM-5:30 PM",
|
||||
"thursday": "9:30 AM-5:30 PM",
|
||||
"friday": "9:30 AM-5:30 PM"
|
||||
},
|
||||
"category": "Garden center",
|
||||
"price_level": null,
|
||||
"description": null,
|
||||
"photos_count": null,
|
||||
"latitude": 38.7328427,
|
||||
"longitude": -120.8263245,
|
||||
"url": "https://www.google.com/maps/place/Appleseed+Horticulture/@38.7328427,-120.8263245,17z/data=!3m1!4b1!4m6!3m5!1s0x809a59dcdca3b659:0x896f9a2b5ac6f8be!8m2!3d38.7328427!4d-120.8263245!16s%2Fg%2F1hhvm3klf?entry=ttu&g_ep=EgoyMDI2MDgxMi4wIKXMDSoASAFQAw%3D%3D",
|
||||
"permanently_closed": false,
|
||||
"temporarily_closed": false
|
||||
},
|
||||
"apple_maps": {
|
||||
"address": "560 Placerville Dr, Placerville, CA 95667",
|
||||
"hours": {
|
||||
"sunday": "10:00 AM-4:00 PM",
|
||||
"saturday": "10:00 AM-4:00 PM",
|
||||
"monday": "9:00 AM-5:1800 PM",
|
||||
"tuesday": "9:00 AM-5:1800 PM",
|
||||
"wednesday": "9:00 AM-5:1800 PM",
|
||||
"thursday": "9:00 AM-5:1800 PM",
|
||||
"friday": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
"coordinates": {
|
||||
"lat": 38.7328373,
|
||||
"lon": -120.8263434
|
||||
},
|
||||
"name": "Appleseed Horticulture",
|
||||
"category": "Shopping",
|
||||
"phone": "+1 (530) 622-5190",
|
||||
"website": "https://www.facebook.com/Appleseed-Horticulture-226187400847519/"
|
||||
},
|
||||
"bing_places": null,
|
||||
"website": {
|
||||
"source_url": "https://www.facebook.com/Appleseed-Horticulture-226187400847519/",
|
||||
"title": "Facebook",
|
||||
"phone_on_page": "8666666666"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"audit_engine": "v1",
|
||||
"timestamp": "2026-08-16T01:15:32.625723+00:00",
|
||||
"business": "Appleseed Horticulture",
|
||||
"summary": {
|
||||
"total_findings": 8,
|
||||
"severity_counts": {
|
||||
"immediate": 2,
|
||||
"high": 1,
|
||||
"medium": 3,
|
||||
"enhancement": 2
|
||||
},
|
||||
"sub_score_issues": {
|
||||
"Digital Identity": 2,
|
||||
"Google Business": 1,
|
||||
"Technical SEO": 2,
|
||||
"Reviews": 1,
|
||||
"Content": 2
|
||||
},
|
||||
"surfaces_ok": {
|
||||
"google_business_profile": true,
|
||||
"apple_maps": true,
|
||||
"website": true
|
||||
},
|
||||
"surfaces_failed": {
|
||||
"bing_places": false
|
||||
},
|
||||
"rating": 4.9,
|
||||
"reviews": 84
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "nap_phone_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Phone number inconsistent across 1 surface(s)",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "(530) 622-5190",
|
||||
"apple": "+1 (530) 622-5190",
|
||||
"website": "8666666666"
|
||||
},
|
||||
"recommendation": "Standardize phone format across all directories. GBP is authoritative."
|
||||
},
|
||||
{
|
||||
"id": "hours_mismatch",
|
||||
"severity": "immediate",
|
||||
"title": "Hours mismatch on 7 day(s) \u2014 GBP is authoritative",
|
||||
"sub_score": "Google Business",
|
||||
"evidence": [
|
||||
{
|
||||
"day": "saturday",
|
||||
"google": "10 AM-5 PM",
|
||||
"apple_maps": "10:00 AM-4:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "sunday",
|
||||
"google": "11 AM-4 PM",
|
||||
"apple_maps": "10:00 AM-4:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple_maps": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple_maps": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple_maps": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple_maps": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple_maps": "9:00 AM-5:1800 PM"
|
||||
}
|
||||
],
|
||||
"recommendation": "Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."
|
||||
},
|
||||
{
|
||||
"id": "missing_jsonld_schema",
|
||||
"severity": "high",
|
||||
"title": "No JSON-LD schema found on website",
|
||||
"sub_score": "Technical SEO",
|
||||
"evidence": {
|
||||
"url": "https://www.facebook.com/Appleseed-Horticulture-226187400847519/",
|
||||
"has_jsonld": false
|
||||
},
|
||||
"recommendation": "Add LocalBusiness/BeautySalon JSON-LD schema. Critical for rich results and local ranking."
|
||||
},
|
||||
{
|
||||
"id": "review_dates_unavailable",
|
||||
"severity": "medium",
|
||||
"title": "Cannot determine review recency \u2014 no dates available in samples",
|
||||
"sub_score": "Reviews",
|
||||
"evidence": {
|
||||
"sources_with_dates": []
|
||||
},
|
||||
"recommendation": "Review date data is not available from current surfaces. Monitor via GBP dashboard directly."
|
||||
},
|
||||
{
|
||||
"id": "category_fragmentation",
|
||||
"severity": "medium",
|
||||
"title": "Category varies across surfaces: garden center, shopping",
|
||||
"sub_score": "Digital Identity",
|
||||
"evidence": {
|
||||
"google": "garden center",
|
||||
"apple": "shopping"
|
||||
},
|
||||
"recommendation": "Align primary category across all directories. GBP primary category is the ranking driver."
|
||||
},
|
||||
{
|
||||
"id": "no_description",
|
||||
"severity": "medium",
|
||||
"title": "No business description found on any surface",
|
||||
"sub_score": "Content",
|
||||
"evidence": {
|
||||
"surfaces_checked": [
|
||||
"google",
|
||||
"bing",
|
||||
"website"
|
||||
]
|
||||
},
|
||||
"recommendation": "Add a keyword-rich description to GBP and website. Descriptions improve click-through and category relevance."
|
||||
},
|
||||
{
|
||||
"id": "no_price_level",
|
||||
"severity": "enhancement",
|
||||
"title": "Price level not set on GBP or Apple Maps",
|
||||
"sub_score": "Content",
|
||||
"evidence": {},
|
||||
"recommendation": "Set price level ($\u2013$$$$) on GBP. Helps users self-select and improves relevance."
|
||||
},
|
||||
{
|
||||
"id": "missing_og_tags",
|
||||
"severity": "enhancement",
|
||||
"title": "No Open Graph tags on website",
|
||||
"sub_score": "Technical SEO",
|
||||
"evidence": {
|
||||
"url": "https://www.facebook.com/Appleseed-Horticulture-226187400847519/"
|
||||
},
|
||||
"recommendation": "Add OG tags for proper link preview on social media and messaging apps."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
# Audit Findings — Appleseed Horticulture
|
||||
|
||||
**Generated:** 2026-08-16 01:15 UTC
|
||||
**Sources checked:** google_business_profile, apple_maps, website
|
||||
**Rating:** 4.9 | **Reviews:** 84
|
||||
|
||||
## Severity Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🟥 Immediate | 2 |
|
||||
| 🟧 High | 1 |
|
||||
| 🟨 Medium | 3 |
|
||||
| 🟩 Enhancement | 2 |
|
||||
| **Total** | **8** |
|
||||
|
||||
## Findings
|
||||
|
||||
### Finding #1: Phone number inconsistent across 1 surface(s)
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Standardize phone format across all directories. GBP is authoritative.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "(530) 622-5190",
|
||||
"apple": "+1 (530) 622-5190",
|
||||
"website": "8666666666"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #2: Hours mismatch on 7 day(s) — GBP is authoritative
|
||||
|
||||
- **Severity:** 🟥 Immediate
|
||||
- **Category:** Google Business
|
||||
- **Recommendation:** Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss.
|
||||
- **Evidence:** ```json
|
||||
[
|
||||
{
|
||||
"day": "saturday",
|
||||
"google": "10 AM-5 PM",
|
||||
"apple_maps": "10:00 AM-4:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "sunday",
|
||||
"google": "11 AM-4 PM",
|
||||
"apple_maps": "10:00 AM-4:00 PM"
|
||||
},
|
||||
{
|
||||
"day": "monday",
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple_maps": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "tuesday",
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple_maps": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "wednesday",
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple_maps": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "thursday",
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple_maps": "9:00 AM-5:1800 PM"
|
||||
},
|
||||
{
|
||||
"day": "friday",
|
||||
"google": "9:30 AM-5:30 PM",
|
||||
"apple_maps": "9:00 AM-5:1800 PM"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Finding #3: No JSON-LD schema found on website
|
||||
|
||||
- **Severity:** 🟧 High
|
||||
- **Category:** Technical SEO
|
||||
- **Recommendation:** Add LocalBusiness/BeautySalon JSON-LD schema. Critical for rich results and local ranking.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"url": "https://www.facebook.com/Appleseed-Horticulture-226187400847519/",
|
||||
"has_jsonld": false
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #4: Cannot determine review recency — no dates available in samples
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Reviews
|
||||
- **Recommendation:** Review date data is not available from current surfaces. Monitor via GBP dashboard directly.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"sources_with_dates": []
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #5: Category varies across surfaces: garden center, shopping
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Digital Identity
|
||||
- **Recommendation:** Align primary category across all directories. GBP primary category is the ranking driver.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"google": "garden center",
|
||||
"apple": "shopping"
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #6: No business description found on any surface
|
||||
|
||||
- **Severity:** 🟨 Medium
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Add a keyword-rich description to GBP and website. Descriptions improve click-through and category relevance.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"surfaces_checked": [
|
||||
"google",
|
||||
"bing",
|
||||
"website"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Finding #7: Price level not set on GBP or Apple Maps
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Content
|
||||
- **Recommendation:** Set price level ($–$$$$) on GBP. Helps users self-select and improves relevance.
|
||||
|
||||
### Finding #8: No Open Graph tags on website
|
||||
|
||||
- **Severity:** 🟩 Enhancement
|
||||
- **Category:** Technical SEO
|
||||
- **Recommendation:** Add OG tags for proper link preview on social media and messaging apps.
|
||||
- **Evidence:** ```json
|
||||
{
|
||||
"url": "https://www.facebook.com/Appleseed-Horticulture-226187400847519/"
|
||||
}
|
||||
```
|
||||
|
||||
## Failed Surfaces
|
||||
|
||||
- ❌ `bing_places` — no data returned
|
||||
@@ -0,0 +1,107 @@
|
||||
# Golden Ticket — Founding Partner Audit Program
|
||||
|
||||
**Date:** 2026-08-17
|
||||
**Owner:** Tony
|
||||
**Status:** Concept → Design phase
|
||||
**Related systems:** VeriPath `audit_engine.py`, `audit_pipeline.sh`, Phoenix Salon (flagship), Digital Operations Partner northstar
|
||||
|
||||
## 1. Core Idea (the lever)
|
||||
|
||||
We are creating a high-signal, physical "Golden Ticket" experience for a very small number of carefully selected local businesses.
|
||||
|
||||
The ticket is not a sales offer. It is a gift of clarity.
|
||||
|
||||
Someone (us) took the time to run a real diagnostic on their digital footprint, surface the highest-leverage problems with evidence, and hand them a clear map — packaged with care. They receive this whether they ever pay us or not.
|
||||
|
||||
This is deliberately framed as a rare, high-trust act of *seeing* the business accurately. In a noisy environment, precise attention feels almost like grace. That feeling is the source of the power.
|
||||
|
||||
## 2. Strategic Intent
|
||||
|
||||
- Create undeniable before/after proof
|
||||
- Generate clean reciprocity and high-quality referrals from well-networked owners
|
||||
- Seed the beginning of deep vertical infrastructure (starting with hair salons)
|
||||
- Position VeriPath / Digital Operations Partner as the party that notices and names the leaks first
|
||||
- Feed the long-term goal of building "clean fuel" (structured, agent-preferable data and patterns) inside specific verticals
|
||||
|
||||
This is not a volume lead-gen tactic. Scarcity and selection quality are non-negotiable.
|
||||
|
||||
## 3. Selection Criteria (sacred filter)
|
||||
|
||||
Only pursue businesses that meet most or all of these:
|
||||
|
||||
- Large, visible gap between current digital state and easily fixable state (so impact is obvious)
|
||||
- Owner or decision-maker has real local network density (Chamber, Rotary, peer owners, suppliers, etc.)
|
||||
- Business sits inside a vertical we intend to go deep on (Phase 1: salons / personal care — Phoenix Salon as living flagship)
|
||||
- Owner personality likely to feel and act on clean reciprocity
|
||||
- Practical ability for us to deliver results
|
||||
|
||||
Target: 5–8 Founding Partner seats total in the first wave. Quality over quantity.
|
||||
|
||||
Operational checklist: `selection-checklist.md`.
|
||||
|
||||
## 4. The Physical Artifact
|
||||
|
||||
- Noticeably better paper stock (heavy, clean texture, restrained design)
|
||||
- Feels intentional and premium without being ostentatious
|
||||
- Creates a micro-pause when held and opened (echo of the metal cash-card unboxing moment that sparked this idea)
|
||||
|
||||
Contents:
|
||||
|
||||
- A short, sharply designed diagnostic report (not a thick binder)
|
||||
- Clear, evidence-first language
|
||||
- Human tone — skilled physician showing the x-ray, not a salesman
|
||||
|
||||
Report template: `report-template.md`.
|
||||
|
||||
## 5. The Two Doors (both must be real)
|
||||
|
||||
**Door A — Paid Path.** We close the critical gaps and install ongoing monitoring + infrastructure work.
|
||||
|
||||
**Door B — Self-Service Path.** We give them a clear, actionable guide on how to fix the issues themselves (or have their team do it), plus vertical-specific playbooks/articles. This path must be genuinely useful, not a teaser. Purity here protects the entire construct from feeling extractive.
|
||||
|
||||
The existence of a real Door B is what keeps reciprocity clean rather than transactional. Minimum content spec: `self-service-outline.md`.
|
||||
|
||||
## 6. Desired Outcomes
|
||||
|
||||
Primary:
|
||||
|
||||
- Visible results for the recipient
|
||||
- High-trust referrals from networked owners
|
||||
- Case studies / proof assets (with permission)
|
||||
|
||||
Secondary / long-game:
|
||||
|
||||
- Accumulation of precise vertical knowledge
|
||||
- Progressive installation of agent-friendly structured data, schema patterns, and clean local signals
|
||||
- Deepening of the salon vertical first (Phoenix as the living reference implementation)
|
||||
|
||||
Seat tracking: `seats.md`.
|
||||
|
||||
## 7. Tone & Principles (non-negotiable)
|
||||
|
||||
- Clarity over persuasion
|
||||
- Evidence over claims
|
||||
- Gift framing over "free trial" or lead magnet framing
|
||||
- No hard close in the initial delivery
|
||||
- Scarcity protected
|
||||
- Self-service path kept real
|
||||
- Selection quality never diluted for volume
|
||||
|
||||
## 8. Connection to Existing Work
|
||||
|
||||
- Leverages the existing VeriPath audit engine (16 deterministic checks, severity tiers, evidence)
|
||||
- Builds on the audit work already done for Phoenix Salon + Spa
|
||||
- Aligns with the Digital Operations Partner northstar: helping regular/underserved business owners regain control of digital footprint and AI visibility
|
||||
- Feeds the broader infrastructure vision of creating clean, agent-preferable data layers inside verticals
|
||||
|
||||
## 9. Deliverables (this directory)
|
||||
|
||||
1. `selection-checklist.md` — selection criteria, operational
|
||||
2. `report-template.md` — report structure + production checklist
|
||||
3. `invitation-copy.md` — cover letter / card language options
|
||||
4. `self-service-outline.md` — Door B minimum content
|
||||
5. `seats.md` — tracking for the 5–8 Founding Partner seats
|
||||
|
||||
## 10. One-Sentence Summary
|
||||
|
||||
We are designing a scarce, high-trust physical diagnostic gift that delivers precise clarity to carefully chosen local businesses, opens both a paid and a genuine self-service door, and begins laying the foundation for deep vertical infrastructure that agents will prefer — starting with salons.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Invitation / Cover Letter — Language Options
|
||||
|
||||
Rules: gift-toned. No "free," no "trial," no "opportunity," no urgency pressure on the recipient. Scarcity is real but quiet. Short.
|
||||
|
||||
## Option A — Card (inside the envelope, ≤ 40 words)
|
||||
|
||||
> We ran a diagnostic on [Business]'s digital footprint.
|
||||
> Inside: what we found, with evidence, and two ways to act on it.
|
||||
> No obligation. This is yours either way.
|
||||
> — [Name], VeriPath
|
||||
|
||||
## Option B — Short letter
|
||||
|
||||
> [Name],
|
||||
>
|
||||
> We've been working with [vertical] businesses in [city], and when we looked at how [Business] shows up — to customers, and to the AI tools people now use to find places — we found real gaps. Fixable ones.
|
||||
>
|
||||
> So we ran the full diagnostic and put the results in this folder. It's yours whether or not you ever talk to us.
|
||||
>
|
||||
> If you want to act on it, there are two doors: we can do the work, or the attached guide lets you or your team do it yourself.
|
||||
>
|
||||
> — [Name]
|
||||
|
||||
## Option C — Envelope one-liner
|
||||
|
||||
> A look at how [Business] shows up online. Evidence inside. Yours either way.
|
||||
|
||||
## Tone check (apply to all)
|
||||
|
||||
- [ ] Reads like a physician showing an x-ray, not a salesman cracking a door
|
||||
- [ ] Zero pressure words (free, opportunity, limited, don't miss, no obligation *except as stated in A*)
|
||||
- [ ] Would still make sense if the owner never paid us
|
||||
- [ ] Name of the business present — a template that could go to anyone fails the program
|
||||
@@ -0,0 +1,40 @@
|
||||
# Golden Ticket Report — Template
|
||||
|
||||
Built on the locked beta pipeline (`multi_scraper → audit_engine → report_generate → report_gate`; see `docs/operations/beta-audit-process.md`). The Golden Ticket report is the synthesized, client-facing artifact: physician tone, x-ray in hand, no sales pitch.
|
||||
|
||||
## Rules
|
||||
|
||||
- **3–5 findings max.** Every finding = plain-English explanation + concrete evidence (quoted field value, screenshot, date). No finding without evidence.
|
||||
- **Tiered language:** Tier 1 (verified) → state as fact. Tier 2 (corroborated) → "appears to," "in our sample."
|
||||
- **Banned language** (same gate as `docs/agents/audit-findings-template.md`): *should, recommend, fix, opportunity, package, solution, proposal, next step.* We show the x-ray; the owner draws conclusions.
|
||||
- No hard close. No pricing. No "free trial" framing anywhere.
|
||||
|
||||
## Structure
|
||||
|
||||
### 1. Recognition
|
||||
One sentence that proves we actually looked. Must be specific to *this* business — if the sentence would fit any other business, rewrite it.
|
||||
|
||||
### 2. What we found (3–5 findings)
|
||||
For each:
|
||||
- **Title** — plain English, no jargon (e.g., "Your hours disagree across three places," not "temporal inconsistency vector")
|
||||
- **What it looks like** — the evidence: the quoted value, the screenshot reference, the date observed
|
||||
- **Who it touches** — customers, or how AI surfaces describe the business
|
||||
|
||||
### 3. Consequence (quiet)
|
||||
Two to four sentences: what this means for customers finding the business, and how AI answer engines currently represent it. State it flat. Let it land.
|
||||
|
||||
### 4. Two doors
|
||||
- **Door A — the work.** "If you want, we close these gaps and keep watching." (No pricing here.)
|
||||
- **Door B — do it yourself.** "The attached guide covers each finding with exact steps and values. It's complete on its own."
|
||||
Both doors real. Neither pitched over the other.
|
||||
|
||||
### 5. Method footer
|
||||
One line: surfaces checked, date, tool. No disclaimer wall.
|
||||
|
||||
## Production checklist (before print)
|
||||
|
||||
- [ ] Every engine finding resolved per beta-audit reviewer loop (VALIDATION.md exists)
|
||||
- [ ] Banned-language gate pass
|
||||
- [ ] Recognition sentence is wrong-for-anyone-else test
|
||||
- [ ] Door B attachment present and standalone-complete
|
||||
- [ ] Findings count 3–5, each with evidence + tiered phrasing
|
||||
@@ -0,0 +1,22 @@
|
||||
# Founding Partner Seats — Wave 1
|
||||
|
||||
Target: 5–8 seats. One row per candidate from the moment they pass `selection-checklist.md` — not when they accept.
|
||||
|
||||
Statuses: `sight` → `contacted` → `delivered` → `door-a` / `door-b` / `declined` → `follow-up` → `closed`
|
||||
|
||||
| Seat | Business | Vertical | Status | Date | Findings (one line) | Referred (name, date) | Proof asset? | Notes |
|
||||
|------|----------|----------|--------|------|---------------------|------------------------|--------------|-------|
|
||||
| 1 | | | | | | | | |
|
||||
| 2 | | | | | | | | |
|
||||
| 3 | | | | | | | | |
|
||||
| 4 | | | | | | | | |
|
||||
| 5 | | | | | | | | |
|
||||
| 6 | | | | | | | | |
|
||||
| 7 | | | | | | | | |
|
||||
| 8 | | | | | | | | |
|
||||
|
||||
Rules:
|
||||
|
||||
- **Referred** column is the primary output metric of this program. Log every name the moment it's mentioned, even if they don't convert.
|
||||
- **Proof asset:** "Y (permission)" or "N". Case studies only with written permission.
|
||||
- Never delete rows. Declined and wrong-fit candidates stay as a record — they inform the filter.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Founding Partner — Selection Checklist
|
||||
|
||||
One page per candidate. Fill it out *before* any invitation goes out.
|
||||
|
||||
## Non-negotiables (either fails → no seat, no exceptions)
|
||||
|
||||
1. **Network density** — owner is a genuine node in the local network (Chamber, Rotary, peer owners, suppliers). "Has a Chamber card" is not evidence; verify who they actually know or move through. This is the referral engine. Without it, the seat produces nothing.
|
||||
2. **Vertical fit** — Phase 1 is salons / personal care only. Other verticals go on a waitlist, never into a seat.
|
||||
|
||||
## Scored criteria (need 4 of 5)
|
||||
|
||||
| # | Criterion | Pass evidence |
|
||||
|---|-----------|---------------|
|
||||
| 1 | Visible gap, easily fixable | Run the existing cold audit (dop-cold-audit skill) before offering. Gap must be obvious AND fixable in ≤ ~30h of work. If impact wouldn't be visible in 30 days, the gap is too deep. |
|
||||
| 2 | Network density | (Non-negotiable #1, scored here too) |
|
||||
| 3 | Vertical fit | (Non-negotiable #2, scored here too) |
|
||||
| 4 | Reciprocity type | Owner talks about other businesses generously, gives rather than collects. A transactional hunter will convert the gift into a haggling session. |
|
||||
| 5 | Deliverability | We can reach the surfaces (GBP owner, site access or owner cooperation) and produce the report in 2 weeks. |
|
||||
|
||||
**Decision rule:** 4/5 AND both non-negotiables pass → seat candidate. Anything less → no. Do not "almost in." A seat given to the wrong owner cannot be taken back and poisons the reference.
|
||||
|
||||
## Stop rules
|
||||
|
||||
- More than 2 candidates in the same sub-market (they'd be each other's competitors; proof assets get muddy).
|
||||
- Owner unreachable after 2 genuine touches.
|
||||
- High gap but no network density → that's a normal lead-gen prospect, not a Founding Partner. Route to standard beta pipeline instead.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Door B — Self-Service Path (Minimum Contents)
|
||||
|
||||
Rule: Door B must be genuinely complete. A teaser Door B contaminates the whole program — this is the single thing that keeps reciprocity clean.
|
||||
|
||||
Format: a per-business playbook built from *that* business's 3–5 report findings. Not a generic ebook. One page per finding.
|
||||
|
||||
## Per finding, each page contains
|
||||
|
||||
1. **What to change** — exact field, exact value. For GBP: the actual name/address format/hours/category/description text, written out. For the website: the exact snippet or line.
|
||||
2. **Where** — the exact screen and menu path (GBP / website CMS / Apple Maps / Bing Places).
|
||||
3. **How to verify** — how the owner confirms it worked: re-check the listing, or re-run the exact AI query we used (prompt included) and see the expected answer.
|
||||
4. **Time** — honest estimate ("~20 minutes").
|
||||
|
||||
## Standing sections
|
||||
|
||||
- **Schema block** — copy-paste JSON-LD (correct LocalBusiness subtype) with their real values, and where it goes in the site.
|
||||
- **Review velocity starter** — how to ask for reviews without violating platform rules (what's allowed / what isn't).
|
||||
- **30-day self-check** — five checks the owner can re-run monthly, including the AI-visibility prompt with the expected answer after fixes land.
|
||||
- **Traps** — one page of what NOT to do in this vertical (common mistakes, e.g., fake review incentives, category spamming, churning GBP).
|
||||
|
||||
## Out of scope for Door B
|
||||
|
||||
Ongoing monitoring, competitor work, anything needing account access we don't hold. If a finding can't be owner-fixed alone, say so in one line and note that it's what Door A covers — as a fact, not a pitch. No "call us if you need help" footer on every page.
|
||||
|
||||
Tone: identical to the report. Physician, not salesman.
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Locked process v1.1: temporal delta — compare two raw captures, emit DELTA.md.
|
||||
|
||||
Usage:
|
||||
audit_diff.py BEFORE_RUN_DIR AFTER_RUN_DIR
|
||||
|
||||
Compares the LATEST capture in each run dir across the fields the audit
|
||||
scores (name, phone, address, hours per day, rating, reviews, website,
|
||||
category, description, schema presence) and writes DELTA.md into the AFTER
|
||||
dir. This is the proof-of-fix artifact: after the client applies the
|
||||
recommendations, re-run the pipeline and the delta is the invoice evidence.
|
||||
|
||||
Exit 0 always (a diff is a result, not an error); DELTA.md lists every
|
||||
change and confirms unchanged fields.
|
||||
"""
|
||||
import json, os, sys, glob
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import report_generate as rg # latest_capture + normalization
|
||||
|
||||
DAYS = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
|
||||
|
||||
def load(d):
|
||||
if os.path.isfile(d):
|
||||
f = d
|
||||
else:
|
||||
f = rg.latest_capture(d)
|
||||
raw = json.load(open(f))
|
||||
src = raw.get("sources", {})
|
||||
g = src.get("google_business_profile") or {}
|
||||
a = src.get("apple_maps") or {}
|
||||
w = src.get("website") if isinstance(src.get("website"), dict) else {}
|
||||
return raw, g, a, w
|
||||
|
||||
def row(label, before, after, fmt=str):
|
||||
b, n = fmt(before), fmt(after)
|
||||
if b == n:
|
||||
return None, (b, b)
|
||||
return (label, b, n), (b, n)
|
||||
|
||||
def schema_present(w):
|
||||
return bool(w.get("schema")) if isinstance(w, dict) else False
|
||||
|
||||
def compare(before_dir, after_dir):
|
||||
raw_b, g_b, a_b, w_b = load(before_dir)
|
||||
raw_a, g_a, a_a, w_a = load(after_dir)
|
||||
changes, same = [], []
|
||||
|
||||
def add(label, b, n, fmt=str):
|
||||
r, vals = row(label, b, n, fmt)
|
||||
(changes if r else same).append(r or (label, vals[0]))
|
||||
|
||||
add("Name (GBP)", g_b.get("name"), g_a.get("name"))
|
||||
add("Phone (GBP)", rg.phone_core(g_b.get("phone")), rg.phone_core(g_a.get("phone")))
|
||||
add("Phone (Apple)", rg.phone_core(a_b.get("phone")), rg.phone_core(a_a.get("phone")))
|
||||
add("Address (GBP)", rg.addr_core(g_b.get("address")), rg.addr_core(g_a.get("address")))
|
||||
add("Address (Apple)", rg.addr_core(a_b.get("address")), rg.addr_core(a_a.get("address")))
|
||||
add("Website (GBP)", g_b.get("website"), g_a.get("website"))
|
||||
add("Rating (GBP)", g_b.get("rating"), g_a.get("rating"))
|
||||
add("Reviews (GBP)", g_b.get("reviews"), g_a.get("reviews"))
|
||||
add("Category (GBP)", g_b.get("category"), g_a.get("category"))
|
||||
add("Description set (GBP)", bool(g_b.get("description")), bool(g_a.get("description")))
|
||||
add("Schema (website)", schema_present(w_b), schema_present(w_a))
|
||||
|
||||
for day in DAYS:
|
||||
gh_b, gh_a = (g_b.get("hours") or {}).get(day), (g_a.get("hours") or {}).get(day)
|
||||
if gh_b != gh_a:
|
||||
changes.append((f"Hours {day} (GBP)", gh_b or "—", gh_a or "—"))
|
||||
ah_b, ah_a = (a_b.get("hours") or {}).get(day), (a_a.get("hours") or {}).get(day)
|
||||
if ah_b != ah_a:
|
||||
changes.append((f"Hours {day} (Apple)", ah_b or "—", ah_a or "—"))
|
||||
|
||||
return changes, same, raw_b, raw_a
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit("usage: audit_diff.py BEFORE_RUN_DIR AFTER_RUN_DIR")
|
||||
before_dir, after_dir = sys.argv[1], sys.argv[2]
|
||||
changes, same, raw_b, raw_a = compare(before_dir, after_dir)
|
||||
L = [f"# Delta: {raw_a.get('name', '?')}", "",
|
||||
f"**Before:** {rg.run_date(raw_b)} (`{os.path.basename(before_dir)}`) ",
|
||||
f"**After:** {rg.run_date(raw_a)} (`{os.path.basename(after_dir)}`) ",
|
||||
"", f"**{len(changes)} field(s) changed** across the audited surfaces.", ""]
|
||||
if changes:
|
||||
L += ["## Changed", "", "| Field | Before | After |", "|-------|--------|-------|"]
|
||||
for label, b, n in changes:
|
||||
L.append(f"| {label} | {b} | {n} |")
|
||||
L.append("")
|
||||
else:
|
||||
L += ["No changes detected between the two captures.", ""]
|
||||
L += ["## Unchanged", ""]
|
||||
L += [f"- {label}: {v}" for label, v in same]
|
||||
L += ["", "*Generated by audit_diff.py (locked process v1.1). Compare only — no judgment.*", ""]
|
||||
out = os.path.join(after_dir, "DELTA.md") if os.path.isdir(after_dir) else \
|
||||
os.path.join(os.path.dirname(os.path.abspath(after_dir)), "DELTA.md")
|
||||
open(out, "w").write("\n".join(L))
|
||||
print(f"{os.path.basename(after_dir)}: {len(changes)} change(s) -> {out}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -63,14 +63,12 @@ def _normalize_phone(phone):
|
||||
|
||||
|
||||
def _normalize_address(addr):
|
||||
"""Lowercase, strip extra whitespace, remove suite abbrev variances, drop commas."""
|
||||
"""Lowercase, strip extra whitespace, remove suite abbrev variances."""
|
||||
if not addr:
|
||||
return ""
|
||||
s = re.sub(r'[\U000E0000-\U000EFFFF]', '', addr or '')
|
||||
s = re.sub(r'\s+', ' ', s.strip().lower())
|
||||
s = re.sub(r'\bst[e]*\.?\b', 'ste', s)
|
||||
s = re.sub(r'\bsuite\b', 'ste', s)
|
||||
s = s.replace(',', '') # ponytail: comma is pure formatting, not a material diff
|
||||
return s
|
||||
|
||||
|
||||
@@ -215,37 +213,57 @@ def check_nap_website(contract):
|
||||
|
||||
|
||||
def check_hours_mismatch(contract):
|
||||
"""Hours consistency. GBP is authoritative."""
|
||||
findings = []
|
||||
gbp_hours = _safe_get(contract, "sources", "google_business_profile", "hours", default={})
|
||||
apple_hours = _safe_get(contract, "sources", "apple_maps", "hours", default={})
|
||||
bing_hours = _safe_get(contract, "sources", "bing_places", "hours", default={})
|
||||
"""Hours consistency vs reference: canonical record (owner-verified truth)
|
||||
if the client has one, else GBP (legacy surface-vs-surface).
|
||||
|
||||
if not gbp_hours:
|
||||
Evidence shape: {reference, reference_hours, provenance, deviations[]}
|
||||
where each deviation day carries the RAW value of every captured surface.
|
||||
"""
|
||||
findings = []
|
||||
from canonical_baseline import resolve_reference, normalize_hours_dict, parse_hours_value
|
||||
|
||||
label, ref_hours, prov = resolve_reference(contract, contract.get("_canonical_record"))
|
||||
ref_norm = normalize_hours_dict(ref_hours or {})
|
||||
if not ref_norm:
|
||||
return findings # nothing to compare against
|
||||
|
||||
mismatched_days = []
|
||||
for other_name, other_hours in [("apple_maps", apple_hours), ("bing_places", bing_hours)]:
|
||||
if not other_hours:
|
||||
sources = contract.get("sources", {})
|
||||
SURF_KEYS = ("google_business_profile", "apple_maps", "bing_places", "website")
|
||||
deviations = []
|
||||
for day, ref_val in sorted(ref_norm.items()):
|
||||
raw_vals = {}
|
||||
for sname in SURF_KEYS:
|
||||
v = (sources.get(sname) or {}).get("hours") or {}
|
||||
if v.get(day) is not None:
|
||||
raw_vals[sname] = v[day]
|
||||
if not raw_vals:
|
||||
continue
|
||||
for day, gbp_val in gbp_hours.items():
|
||||
if day in other_hours:
|
||||
other_val = other_hours[day]
|
||||
if not _hours_overlap(gbp_val, other_val):
|
||||
mismatched_days.append({
|
||||
"day": day,
|
||||
"google": gbp_val,
|
||||
other_name: other_val
|
||||
})
|
||||
# ponytail: skip the reference surface itself in legacy mode (it can't deviate from itself anyway)
|
||||
bad = any(
|
||||
parse_hours_value(v) is not None and parse_hours_value(v) != ref_val
|
||||
for v in raw_vals.values()
|
||||
)
|
||||
if bad:
|
||||
entry = {"day": day, "reference": ref_val}
|
||||
entry.update(raw_vals)
|
||||
deviations.append(entry)
|
||||
|
||||
if mismatched_days:
|
||||
if deviations:
|
||||
suffix = "owner-verified hours" if prov else "GBP is authoritative"
|
||||
findings.append({
|
||||
"id": "hours_mismatch",
|
||||
"severity": SEVERITY_IMMEDIATE,
|
||||
"title": f"Hours mismatch on {len(mismatched_days)} day(s) — GBP is authoritative",
|
||||
"title": f"Hours mismatch on {len(deviations)} day(s) — {suffix}",
|
||||
"sub_score": GOOGLE_BUSINESS,
|
||||
"evidence": mismatched_days,
|
||||
"recommendation": "Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."
|
||||
"evidence": {
|
||||
"reference": label,
|
||||
"reference_hours": ref_norm,
|
||||
"provenance": prov,
|
||||
"deviations": deviations,
|
||||
},
|
||||
"recommendation": ("Correct hours on the deviating surfaces to match the owner-verified schedule (canonical record)."
|
||||
if prov else
|
||||
"Correct hours on non-GBP surfaces to match GBP. Mismatched hours cause customer complaints and trust loss."),
|
||||
})
|
||||
|
||||
return findings
|
||||
@@ -321,6 +339,76 @@ def check_review_count_delta(contract):
|
||||
return findings
|
||||
|
||||
|
||||
def check_onpage_title_meta(contract):
|
||||
"""On-page title/meta audit (technical SEO): presence, length, name, phone.
|
||||
|
||||
Reads the website fields captured at scrape time (title, description,
|
||||
phone_on_page) so the audit is reproducible from the capture. Skips
|
||||
cleanly when no website surface was captured."""
|
||||
findings = []
|
||||
ws = _safe_get(contract, "sources", "website", default={}) or {}
|
||||
title = ws.get("title") or ""
|
||||
desc = ws.get("description") or ""
|
||||
if not title and not desc:
|
||||
return findings # no website surface captured
|
||||
|
||||
name = (contract.get("name") or "").lower()
|
||||
phone_norm = _normalize_phone(contract.get("phone") or "")
|
||||
page_phone_norm = _normalize_phone(ws.get("phone_on_page") or "")
|
||||
|
||||
# <title> presence & length (Google truncates ~60 chars)
|
||||
if not title:
|
||||
findings.append({
|
||||
"id": "missing_title", "severity": SEVERITY_HIGH, "sub_score": TECHNICAL_SEO,
|
||||
"title": "Website has no <title> tag — search engines fall back to the URL.",
|
||||
"evidence": {"title": ""},
|
||||
"recommendation": "Add a descriptive <title> tag including the business name and main service.",
|
||||
})
|
||||
else:
|
||||
if len(title) > 65:
|
||||
findings.append({
|
||||
"id": "title_too_long", "severity": SEVERITY_ENHANCEMENT, "sub_score": TECHNICAL_SEO,
|
||||
"title": f"Title tag is {len(title)} chars (>65) — truncated in Google results.",
|
||||
"evidence": {"title": title, "length": len(title)},
|
||||
"recommendation": "Shorten the <title> to ~50-60 characters.",
|
||||
})
|
||||
if name and name not in title.lower():
|
||||
findings.append({
|
||||
"id": "title_missing_name", "severity": SEVERITY_MEDIUM, "sub_score": TECHNICAL_SEO,
|
||||
"title": "Title tag does not contain the business name.",
|
||||
"evidence": {"title": title, "business_name": name},
|
||||
"recommendation": "Include the business name in the <title> for brand recognition.",
|
||||
})
|
||||
|
||||
# Meta description presence & length (Google truncates ~160 chars)
|
||||
if not desc:
|
||||
findings.append({
|
||||
"id": "missing_meta_description", "severity": SEVERITY_MEDIUM, "sub_score": TECHNICAL_SEO,
|
||||
"title": "No meta description — Google may use arbitrary page text as the snippet.",
|
||||
"evidence": {"description": ""},
|
||||
"recommendation": "Write a 150-160 character meta description naming the business and service.",
|
||||
})
|
||||
elif len(desc) > 170:
|
||||
findings.append({
|
||||
"id": "meta_description_too_long", "severity": SEVERITY_ENHANCEMENT, "sub_score": TECHNICAL_SEO,
|
||||
"title": f"Meta description is {len(desc)} chars (>170) — truncated in results.",
|
||||
"evidence": {"length": len(desc)},
|
||||
"recommendation": "Shorten the meta description to ~150-160 characters.",
|
||||
})
|
||||
|
||||
# On-page NAP: flag only when a phone WAS captured and it differs from the
|
||||
# business phone (evidence-based; no capture = can't conclude, don't flag).
|
||||
if phone_norm and page_phone_norm and phone_norm != page_phone_norm:
|
||||
findings.append({
|
||||
"id": "phone_not_on_page", "severity": SEVERITY_MEDIUM, "sub_score": DIGITAL_IDENTITY,
|
||||
"title": "Phone number found on the website differs from the business phone (on-page NAP gap).",
|
||||
"evidence": {"business_phone": phone_norm, "phone_on_page": page_phone_norm},
|
||||
"recommendation": "Make the website phone match the business phone in the header/footer.",
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def check_review_velocity(contract):
|
||||
"""Review recency — are recent reviews flowing?"""
|
||||
findings = []
|
||||
@@ -333,7 +421,7 @@ def check_review_velocity(contract):
|
||||
if not dates:
|
||||
findings.append({
|
||||
"id": "review_dates_unavailable",
|
||||
"severity": SEVERITY_MEDIUM,
|
||||
"severity": SEVERITY_ENHANCEMENT, # tooling limitation, not a business finding (matches classify() FP in report_generate.py)
|
||||
"title": "Cannot determine review recency — no dates available in samples",
|
||||
"sub_score": REVIEWS,
|
||||
"evidence": {"sources_with_dates": []},
|
||||
@@ -364,62 +452,78 @@ def check_review_velocity(contract):
|
||||
return findings
|
||||
|
||||
|
||||
def _service_keywords(contract):
|
||||
"""Derive the business's own service/category vocabulary (vertical-agnostic).
|
||||
|
||||
Grounded in what the business actually sells — its category, description,
|
||||
and name — so the specificity signal adapts to any vertical instead of
|
||||
assuming one (the old version hardcoded salon terms).
|
||||
"""
|
||||
words = set()
|
||||
# Top-level capture fields are the most reliable vocabulary source
|
||||
for field in ("name", "category", "description"):
|
||||
val = contract.get(field) or ""
|
||||
for w in re.findall(r"[a-z]{3,}", val.lower()):
|
||||
words.add(w)
|
||||
for src in ["google_business_profile", "apple_maps", "bing_places", "website"]:
|
||||
data = _safe_get(contract, "sources", src, default={}) or {}
|
||||
for field in ("category", "description", "name", "title"):
|
||||
val = data.get(field) or ""
|
||||
for w in re.findall(r"[a-z]{3,}", val.lower()):
|
||||
words.add(w)
|
||||
# Drop generic words that don't indicate a specific service/product
|
||||
stop = {"the", "and", "for", "with", "this", "that", "your", "you", "our",
|
||||
"are", "was", "have", "has", "been", "will", "would", "like", "get",
|
||||
"got", "good", "great", "best", "very", "really", "service",
|
||||
"services", "business", "place", "store", "shop", "location", "area",
|
||||
"city", "staff", "customer", "customers", "experience", "quality",
|
||||
"professional", "professionals", "helpful", "friendly", "recommend",
|
||||
"recommended", "price", "prices", "paid", "cost", "costs", "time",
|
||||
"times", "day", "days", "week", "weeks", "month", "months", "year",
|
||||
"years", "online", "presence", "audit", "veripath"}
|
||||
return words - stop
|
||||
|
||||
|
||||
def check_review_text_quality(contract):
|
||||
"""Check if reviews mention specific services/practitioners (E-E-A-T signal)."""
|
||||
"""Check if reviews mention the business's own services/products (E-E-A-T signal).
|
||||
|
||||
Vertical-agnostic: the keyword set is derived from the business's own
|
||||
category/description/name, not a hardcoded vertical.
|
||||
"""
|
||||
findings = []
|
||||
all_samples = []
|
||||
for src in ["apple_maps", "bing_places"]:
|
||||
samples = _safe_get(contract, "sources", src, "reviews_sample", default=[])
|
||||
all_samples.extend(samples or [])
|
||||
|
||||
if not all_samples:
|
||||
if len(all_samples) < 3:
|
||||
return findings
|
||||
|
||||
keywords = _service_keywords(contract)
|
||||
if not keywords:
|
||||
return findings # no vocabulary to match against; can't judge specificity
|
||||
|
||||
specific_signals = 0
|
||||
for r in all_samples:
|
||||
text = (r.get('text') or '').lower()
|
||||
if any(word in text for word in ['ashley', 'grace', 'mallory', 'facial', 'hair', 'waxing', 'lashes', 'spray tan', 'massage']):
|
||||
if any(w in text for w in keywords):
|
||||
specific_signals += 1
|
||||
|
||||
ratio = specific_signals / len(all_samples) if all_samples else 0
|
||||
if ratio < 0.5 and len(all_samples) >= 3:
|
||||
ratio = specific_signals / len(all_samples)
|
||||
if ratio < 0.5:
|
||||
findings.append({
|
||||
"id": "reviews_low_specificity",
|
||||
"severity": SEVERITY_ENHANCEMENT,
|
||||
"title": f"Only {ratio:.0%} of sample reviews mention specific services or practitioners",
|
||||
"title": f"Only {ratio:.0%} of sample reviews mention specific services or staff",
|
||||
"sub_score": REVIEWS,
|
||||
"evidence": {"sample_size": len(all_samples), "specific_count": specific_signals},
|
||||
"recommendation": "Encourage detailed reviews mentioning services and staff. Specific reviews rank higher and convert better."
|
||||
"evidence": {"sample_size": len(all_samples), "specific_count": specific_signals,
|
||||
"keyword_basis": "category+description+name"},
|
||||
"recommendation": "Encourage reviews that name specific services, products, or staff. Specific reviews rank higher and convert better."
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
# ponytail: small explicit synonym map; extend when new variants appear in the wild
|
||||
_CATEGORY_SYNONYMS = {
|
||||
"beauty salon": "beauty salon",
|
||||
"beauty and spa": "beauty salon",
|
||||
"beauty & spa": "beauty salon",
|
||||
"beauty & spa": "beauty salon",
|
||||
"salon and spa": "beauty salon",
|
||||
"hair salon": "hair salon",
|
||||
"hair & beauty salon": "hair salon",
|
||||
"hair and beauty salon": "hair salon",
|
||||
"day spa": "day spa",
|
||||
"spa": "spa",
|
||||
"medical spa": "medical spa",
|
||||
"barber shop": "barber shop",
|
||||
"barbershop": "barber shop",
|
||||
}
|
||||
|
||||
|
||||
def _normalize_category(cat):
|
||||
if not cat:
|
||||
return ""
|
||||
c = re.sub(r'\s+', ' ', cat.strip().lower()).replace('&', '&')
|
||||
return _CATEGORY_SYNONYMS.get(c, c)
|
||||
|
||||
|
||||
def check_category_alignment(contract):
|
||||
"""Category consistency and fragmentation across surfaces."""
|
||||
findings = []
|
||||
@@ -430,7 +534,7 @@ def check_category_alignment(contract):
|
||||
data = _safe_get(contract, "sources", src_data, default={})
|
||||
cat = data.get('category')
|
||||
if cat:
|
||||
categories[src_name] = _normalize_category(cat)
|
||||
categories[src_name] = cat.lower().strip()
|
||||
|
||||
if len(categories) < 2:
|
||||
return findings
|
||||
@@ -567,13 +671,32 @@ def check_website_schema(contract):
|
||||
})
|
||||
return findings
|
||||
|
||||
# Fetch and check for JSON-LD
|
||||
try:
|
||||
import urllib.request
|
||||
req = urllib.request.Request(website_url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
html = resp.read().decode("utf-8", errors="replace")
|
||||
|
||||
# Prefer capture-time evidence (reproducible audit). Live refetch only for
|
||||
# pre-fix captures that lack the fields.
|
||||
ws = _safe_get(contract, "sources", "website", default={}) or {}
|
||||
if "has_jsonld" in ws:
|
||||
has_jsonld = bool(ws.get("has_jsonld"))
|
||||
types = [t.lower() for t in ws.get("schema_types") or []]
|
||||
has_local_business = any(t in ("localbusiness", "beautysalon", "dayspa", "healthandbeautybusiness") for t in types)
|
||||
has_canonical = bool(ws.get("canonical"))
|
||||
has_og = bool(ws.get("has_og"))
|
||||
else:
|
||||
# Fetch and check for JSON-LD (legacy captures)
|
||||
try:
|
||||
import urllib.request
|
||||
req = urllib.request.Request(website_url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
html = resp.read().decode("utf-8", errors="replace")
|
||||
except Exception as e:
|
||||
findings.append({
|
||||
"id": "website_unreachable",
|
||||
"severity": SEVERITY_HIGH,
|
||||
"title": f"Website fetch failed: {e}",
|
||||
"sub_score": TECHNICAL_SEO,
|
||||
"evidence": {"url": website_url, "error": str(e)},
|
||||
"recommendation": "Verify website is live and accessible. A broken site kills all local signals."
|
||||
})
|
||||
return findings
|
||||
# Check for JSON-LD
|
||||
has_jsonld = bool(re.search(r'<script[^>]*type=["\']application/ld\+json["\']', html, re.IGNORECASE))
|
||||
has_local_business = bool(re.search(r'"@type"\s*:\s*"(?:LocalBusiness|BeautySalon|DaySpa|HealthAndBeautyBusiness)', html, re.IGNORECASE))
|
||||
@@ -584,53 +707,43 @@ def check_website_schema(contract):
|
||||
# Check for OG tags
|
||||
has_og = bool(re.search(r'<meta[^>]*property=["\']og:', html, re.IGNORECASE))
|
||||
|
||||
if not has_jsonld:
|
||||
findings.append({
|
||||
"id": "missing_jsonld_schema",
|
||||
"severity": SEVERITY_HIGH,
|
||||
"title": "No JSON-LD schema found on website",
|
||||
"sub_score": TECHNICAL_SEO,
|
||||
"evidence": {"url": website_url, "has_jsonld": False},
|
||||
"recommendation": "Add LocalBusiness/BeautySalon JSON-LD schema. Critical for rich results and local ranking."
|
||||
})
|
||||
elif not has_local_business:
|
||||
findings.append({
|
||||
"id": "schema_wrong_type",
|
||||
"severity": SEVERITY_MEDIUM,
|
||||
"title": "JSON-LD present but no LocalBusiness/BeautySalon type",
|
||||
"sub_score": TECHNICAL_SEO,
|
||||
"evidence": {"url": website_url, "has_jsonld": True, "has_local_business": False},
|
||||
"recommendation": "Ensure schema includes LocalBusiness or BeautySalon @type for local search relevance."
|
||||
})
|
||||
|
||||
if not has_canonical:
|
||||
findings.append({
|
||||
"id": "missing_canonical",
|
||||
"severity": SEVERITY_MEDIUM,
|
||||
"title": "No canonical URL tag on website",
|
||||
"sub_score": TECHNICAL_SEO,
|
||||
"evidence": {"url": website_url},
|
||||
"recommendation": "Add <link rel='canonical' href='...'> to prevent duplicate content issues."
|
||||
})
|
||||
|
||||
if not has_og:
|
||||
findings.append({
|
||||
"id": "missing_og_tags",
|
||||
"severity": SEVERITY_ENHANCEMENT,
|
||||
"title": "No Open Graph tags on website",
|
||||
"sub_score": TECHNICAL_SEO,
|
||||
"evidence": {"url": website_url},
|
||||
"recommendation": "Add OG tags for proper link preview on social media and messaging apps."
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if not has_jsonld:
|
||||
findings.append({
|
||||
"id": "website_unreachable",
|
||||
"id": "missing_jsonld_schema",
|
||||
"severity": SEVERITY_HIGH,
|
||||
"title": f"Website fetch failed: {e}",
|
||||
"title": "No JSON-LD schema found on website",
|
||||
"sub_score": TECHNICAL_SEO,
|
||||
"evidence": {"url": website_url, "error": str(e)},
|
||||
"recommendation": "Verify website is live and accessible. A broken site kills all local signals."
|
||||
"evidence": {"url": website_url, "has_jsonld": False},
|
||||
"recommendation": "Add LocalBusiness/BeautySalon JSON-LD schema. Critical for rich results and local ranking."
|
||||
})
|
||||
elif not has_local_business:
|
||||
findings.append({
|
||||
"id": "schema_wrong_type",
|
||||
"severity": SEVERITY_MEDIUM,
|
||||
"title": "JSON-LD present but no LocalBusiness/BeautySalon type",
|
||||
"sub_score": TECHNICAL_SEO,
|
||||
"evidence": {"url": website_url, "has_jsonld": True, "has_local_business": False},
|
||||
"recommendation": "Ensure schema includes LocalBusiness or BeautySalon @type for local search relevance."
|
||||
})
|
||||
|
||||
if not has_canonical:
|
||||
findings.append({
|
||||
"id": "missing_canonical",
|
||||
"severity": SEVERITY_MEDIUM,
|
||||
"title": "No canonical URL tag on website",
|
||||
"sub_score": TECHNICAL_SEO,
|
||||
"evidence": {"url": website_url},
|
||||
"recommendation": "Add <link rel='canonical' href='...'> to prevent duplicate content issues."
|
||||
})
|
||||
|
||||
if not has_og:
|
||||
findings.append({
|
||||
"id": "missing_og_tags",
|
||||
"severity": SEVERITY_ENHANCEMENT,
|
||||
"title": "No Open Graph tags on website",
|
||||
"sub_score": TECHNICAL_SEO,
|
||||
"evidence": {"url": website_url},
|
||||
"recommendation": "Add OG tags for proper link preview on social media and messaging apps."
|
||||
})
|
||||
|
||||
return findings
|
||||
@@ -693,6 +806,7 @@ ALL_CHECKS = [
|
||||
check_price_level,
|
||||
check_photos,
|
||||
check_website_schema,
|
||||
check_onpage_title_meta,
|
||||
check_google_maps_url,
|
||||
check_coordinates_precision,
|
||||
]
|
||||
@@ -830,6 +944,18 @@ def main():
|
||||
with open(filepath) as f:
|
||||
contract = json.load(f)
|
||||
|
||||
# Canonical baseline: owner-verified record from Gitea (None -> legacy GBP reference)
|
||||
try:
|
||||
from canonical_baseline import fetch_canonical_record, canonical_hours
|
||||
record = fetch_canonical_record(contract.get("name") or "")
|
||||
if record and canonical_hours(record):
|
||||
contract["_canonical_record"] = record
|
||||
print(f" [OK] Canonical baseline: {record['_path']} @ {record['_commit']}", file=sys.stderr)
|
||||
else:
|
||||
print(" [INFO] No verified canonical record — legacy surface-vs-surface reference", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f" [WARN] canonical fetch failed ({e}) — legacy reference", file=sys.stderr)
|
||||
|
||||
findings = analyze(contract)
|
||||
summary = summarize(findings, contract)
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ BUSINESS="${1:?Usage: audit_pipeline.sh \"Business Name\" \"City, ST\" [output-d
|
||||
LOCATION="${2:?Usage: audit_pipeline.sh \"Business Name\" \"City, ST\" [output-dir]}"
|
||||
OUTPUT_DIR="${3:-}"
|
||||
|
||||
# Scraper deps live in a separate uv project (no pyproject in this dir)
|
||||
export UV_PROJECT="${UV_PROJECT:-$HOME/deps/google-maps-scraper}"
|
||||
|
||||
# Temporary working dir for this run
|
||||
WORK_DIR=$(mktemp -d /tmp/veripath.XXXXXX)
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
@@ -18,15 +21,22 @@ ENGINE_ARGS=("--md")
|
||||
|
||||
echo "[$(date +%T)] Scraping: $BUSINESS @ $LOCATION"
|
||||
|
||||
# Step 1: Multi-surface extraction (3rd arg = output DIR; scraper writes <dir>/multi_surface.json/<file>.json)
|
||||
# Step 1: Multi-surface extraction (3rd arg is an output DIR)
|
||||
uv run python3 "$SCRIPT_DIR/multi_scraper.py" "$BUSINESS" "$LOCATION" "$WORK_DIR"
|
||||
SCRAPER_OUT=$(find "$WORK_DIR" -name "*multi_surface*.json" | head -1)
|
||||
if [ ! -s "$SCRAPER_OUT" ]; then
|
||||
if [ -z "$SCRAPER_OUT" ] || [ ! -s "$SCRAPER_OUT" ]; then
|
||||
echo "[FAIL] multi_scraper.py produced no output" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[OK] Extraction complete: $SCRAPER_OUT"
|
||||
|
||||
# Persist the raw capture into the run dir (WORK_DIR is wiped on exit;
|
||||
# report_generate / report_gate need it for evidence validation)
|
||||
if [ -n "$OUTPUT_DIR" ]; then
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
cp "$SCRAPER_OUT" "$OUTPUT_DIR/$(basename "$SCRAPER_OUT")"
|
||||
fi
|
||||
|
||||
# Step 2: Audit engine
|
||||
if [ -n "$OUTPUT_DIR" ]; then
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
@@ -38,3 +48,12 @@ else
|
||||
echo "[$(date +%T)] Auditing (stdout)"
|
||||
uv run python3 "$SCRIPT_DIR/audit_engine.py" "$SCRAPER_OUT" "${ENGINE_ARGS[@]}"
|
||||
fi
|
||||
|
||||
# Step 3: report + strict delivery gate (one-command e2e)
|
||||
if [ -n "$OUTPUT_DIR" ]; then
|
||||
echo "[$(date +%T)] Building report"
|
||||
uv run python3 "$SCRIPT_DIR/report_generate.py" "$OUTPUT_DIR"
|
||||
echo "[$(date +%T)] Running delivery gate (strict)"
|
||||
uv run python3 "$SCRIPT_DIR/report_gate.py" "$OUTPUT_DIR" --strict
|
||||
echo "[GATE DONE]"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Canonical baseline for VeriPath audits.
|
||||
|
||||
Owner-verified ground truth lives on Gitea (Tony_tech/veripath,
|
||||
docs/clients/<slug>/canonical-business-record-vX.Y.Z.json). Only fields with
|
||||
status == "verified" qualify as diff baseline. When no record exists (or the
|
||||
fetch fails), callers fall back to surface-vs-surface comparison.
|
||||
|
||||
Shared by audit_engine.py, report_generate.py, report_gate.py so all three
|
||||
derive the same reference and the same per-day consistency verdict.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import urllib.request
|
||||
|
||||
GITEA_HOST = os.environ.get("GITEA_HOST", "http://localhost:3000")
|
||||
GITEA_REPO = os.environ.get("GITEA_REPO", "Tony_tech/veripath")
|
||||
TOKEN_FILE = os.path.expanduser("~/.hermes/profiles/leonard/.env")
|
||||
|
||||
_CLOSED = {"closed", "none", "n/a", "", "-", "—"}
|
||||
_DAY_MAP = {
|
||||
"monday": "monday", "tuesday": "tuesday", "wednesday": "wednesday",
|
||||
"thursday": "thursday", "friday": "friday", "saturday": "saturday",
|
||||
"sunday": "sunday", "mon": "monday", "tue": "tuesday", "tues": "tuesday",
|
||||
"wed": "wednesday", "thu": "thursday", "thur": "thursday", "thurs": "thursday",
|
||||
"fri": "friday", "sat": "saturday", "sun": "sunday",
|
||||
}
|
||||
|
||||
|
||||
def _gitea_token():
|
||||
try:
|
||||
for line in open(TOKEN_FILE):
|
||||
if line.startswith("GITEA_ACCESS_TOKEN="):
|
||||
return line.strip().split("=", 1)[1].strip()
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _gitea_get(path, token):
|
||||
url = f"{GITEA_HOST}/api/v1/repos/{GITEA_REPO}/contents/{path}"
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def slugify(name):
|
||||
s = re.sub(r"[^a-zA-Z0-9]+", "-", (name or "").strip())
|
||||
return s.strip("-").lower()
|
||||
|
||||
|
||||
def fetch_canonical_record(business_name):
|
||||
"""Fetch the latest canonical record for a client. None if absent.
|
||||
|
||||
ponytail: prefix-match longest-slug-first against actual client dirs
|
||||
("phoenix-salon-spa" -> dir "phoenix-salons"); upgrade to explicit
|
||||
record_id lookup when client naming gets fuzzy.
|
||||
"""
|
||||
token = _gitea_token()
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
entries = _gitea_get("docs/clients", token)
|
||||
except Exception:
|
||||
return None
|
||||
client_dirs = sorted(e["name"] for e in entries if e.get("type") == "dir")
|
||||
words = slugify(business_name).split("-")
|
||||
for k in range(len(words), 0, -1):
|
||||
cand = "-".join(words[:k])
|
||||
for d in client_dirs:
|
||||
if not (d == cand or d.startswith(cand)):
|
||||
continue
|
||||
try:
|
||||
files = _gitea_get(f"docs/clients/{d}", token)
|
||||
except Exception:
|
||||
continue
|
||||
names = sorted(
|
||||
(e["name"] for e in files
|
||||
if e["type"] == "file" and re.match(r"canonical-business-record-v[\d.]+\.json$", e["name"])),
|
||||
key=lambda n: [int(x) for x in re.search(r"v([\d.]+)\.json$", n).group(1).split(".")])
|
||||
if not names:
|
||||
continue
|
||||
body = _gitea_get(f"docs/clients/{d}/{names[-1]}", token)
|
||||
data = json.loads(base64_decode(body["content"]))
|
||||
data["_path"] = f"docs/clients/{d}/{names[-1]}"
|
||||
data["_commit"] = (body.get("last_commit_sha") or "")[:7]
|
||||
return data
|
||||
return None
|
||||
|
||||
|
||||
def base64_decode(s):
|
||||
import base64
|
||||
return base64.b64decode(s).decode("utf-8")
|
||||
|
||||
|
||||
def canonical_hours(record):
|
||||
"""Return verified hours.regular_hours as {day: "HH:MM-HH:MM"|"closed"}, else None."""
|
||||
if not record:
|
||||
return None
|
||||
block = (record.get("domains", {}).get("hours", {}) or {}).get("regular_hours") or {}
|
||||
if block.get("status") != "verified":
|
||||
return None
|
||||
value = block.get("value") or {}
|
||||
days = {}
|
||||
for day, val in value.items():
|
||||
d = _DAY_MAP.get(day.lower())
|
||||
if not d:
|
||||
continue
|
||||
if isinstance(val, dict):
|
||||
if val.get("open") in (None, "", "null") or str(val.get("open", "")).lower() in _CLOSED:
|
||||
days[d] = "closed"
|
||||
else:
|
||||
days[d] = f"{val['open']}-{val.get('close')}"
|
||||
elif str(val).strip().lower() in _CLOSED:
|
||||
days[d] = "closed"
|
||||
else:
|
||||
norm = parse_hours_value(val)
|
||||
if norm:
|
||||
days[d] = norm
|
||||
return days or None
|
||||
|
||||
|
||||
def canonical_provenance(record):
|
||||
"""Compact provenance for reports: where the truth came from."""
|
||||
if not record:
|
||||
return None
|
||||
block = (record.get("domains", {}).get("hours", {}) or {}).get("regular_hours") or {}
|
||||
return {
|
||||
"record": record.get("_path"),
|
||||
"record_id": record.get("record_id"),
|
||||
"verified_at": block.get("verified_at"),
|
||||
"fresh_until": block.get("fresh_until"),
|
||||
"commit": record.get("_commit"),
|
||||
"sources": block.get("sources") or [],
|
||||
}
|
||||
|
||||
|
||||
def parse_hours_value(val):
|
||||
"""Normalize one day's hours to a canonical string, or 'closed'.
|
||||
|
||||
Handles: "Closed"/"closed", "9 AM-6 PM", "9 AM - 6 PM", "10:00 AM-6:00 PM",
|
||||
ISO "09:00-18:00". Returns None when unparseable (comparison skips the day).
|
||||
"""
|
||||
if val is None:
|
||||
return None
|
||||
s = str(val).strip()
|
||||
if s.lower() in _CLOSED:
|
||||
return "closed"
|
||||
times = re.split(r"\s*[-–—]\s*", s)
|
||||
if len(times) != 2:
|
||||
return None
|
||||
parts = []
|
||||
for t in times:
|
||||
m = re.match(r"^(\d{1,2}):?(\d{2})?\s*(am|pm)?$", t.strip().lower().replace(".", ""))
|
||||
if not m:
|
||||
return None
|
||||
hh = int(m.group(1))
|
||||
mm = m.group(2) or "00"
|
||||
ap = m.group(3)
|
||||
if ap == "pm" and hh != 12:
|
||||
hh += 12
|
||||
elif ap == "am" and hh == 12:
|
||||
hh = 0
|
||||
parts.append(f"{hh:02d}:{mm}")
|
||||
return f"{parts[0]}-{parts[1]}"
|
||||
|
||||
|
||||
def normalize_hours_dict(hours):
|
||||
"""{any-day: any-format} -> {canonical-day: canonical-value|'closed'}. Unparseable days dropped."""
|
||||
out = {}
|
||||
for day, val in (hours or {}).items():
|
||||
d = _DAY_MAP.get(str(day).strip().lower())
|
||||
if not d:
|
||||
continue
|
||||
norm = parse_hours_value(val)
|
||||
if norm is not None:
|
||||
out[d] = norm
|
||||
return out
|
||||
|
||||
|
||||
def hours_consistent(a_val, b_val):
|
||||
"""True if two already-normalized day values agree (or either is missing)."""
|
||||
if a_val is None or b_val is None:
|
||||
return True # can't compare
|
||||
return a_val == b_val
|
||||
|
||||
|
||||
def hours_deviations(reference_hours, surface_hours):
|
||||
"""Days where the surface disagrees with the reference. Both dicts normalized."""
|
||||
ref = normalize_hours_dict(reference_hours)
|
||||
surf = normalize_hours_dict(surface_hours)
|
||||
dev = []
|
||||
for day in sorted(set(ref) | set(surf)):
|
||||
rv, sv = ref.get(day), surf.get(day)
|
||||
if rv is not None and sv is not None and rv != sv:
|
||||
dev.append({"day": day, "reference": rv, "surface": sv})
|
||||
return dev
|
||||
|
||||
|
||||
def resolve_reference(contract, record=None):
|
||||
"""Pick the hours reference for an audit.
|
||||
|
||||
Returns (label, reference_hours, provenance|None):
|
||||
- canonical record with verified hours -> owner truth
|
||||
- otherwise -> primary source (legacy surface-vs-surface)
|
||||
"""
|
||||
ch = canonical_hours(record)
|
||||
if ch:
|
||||
return ("canonical record (owner-verified ground truth)", ch, canonical_provenance(record))
|
||||
primary = contract.get("primary_source") or "google_business_profile"
|
||||
ph = (contract.get("sources", {}).get(primary) or {}).get("hours")
|
||||
if ph:
|
||||
label = "Google Business Profile" if primary == "google_business_profile" else primary
|
||||
return (label, ph, None)
|
||||
return (None, None, None)
|
||||
|
||||
|
||||
def jsonld_hours(html):
|
||||
"""Extract per-day hours from JSON-LD (LocalBusiness.openingHours(Specification))."""
|
||||
days = {}
|
||||
for m in re.finditer(r"<script[^>]*type=[\"']application/ld\+json[\"'][^>]*>(.*?)</script>", html, re.DOTALL | re.IGNORECASE):
|
||||
try:
|
||||
data = json.loads(m.group(1).strip())
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
continue
|
||||
nodes = data if isinstance(data, list) else [data]
|
||||
for node in nodes:
|
||||
if not isinstance(node, dict):
|
||||
continue
|
||||
oh = node.get("openingHours")
|
||||
if isinstance(oh, list):
|
||||
for entry in oh:
|
||||
m = re.match(r"^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*(\d{2}:\d{2})-(\d{2}:\d{2})$", str(entry).strip())
|
||||
if m:
|
||||
days[_DAY_MAP[m.group(1).lower()]] = f"{m.group(2)}-{m.group(3)}"
|
||||
ohs = node.get("openingHoursSpecification")
|
||||
if isinstance(ohs, list):
|
||||
for spec in ohs:
|
||||
if not isinstance(spec, dict):
|
||||
continue
|
||||
dts = spec.get("dayOfWeek")
|
||||
dts = dts if isinstance(dts, list) else [dts]
|
||||
for dt in dts:
|
||||
d = _DAY_MAP.get(str(dt).strip().lower())
|
||||
if d and spec.get("opens") and spec.get("closes"):
|
||||
days[d] = f"{spec['opens'][:5]}-{spec['closes'][:5]}"
|
||||
return days
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Self-check: fails if any of the normalizations above regress
|
||||
assert parse_hours_value("9 AM-6 PM") == "09:00-18:00"
|
||||
assert parse_hours_value("9 AM - 6 PM") == "09:00-18:00"
|
||||
assert parse_hours_value("10:00 AM-6:00 PM") == "10:00-18:00"
|
||||
assert parse_hours_value("Closed") == "closed"
|
||||
assert parse_hours_value("09:00-18:00") == "09:00-18:00"
|
||||
assert parse_hours_value("9am–5pm") is not None
|
||||
assert normalize_hours_dict({"Monday": "9 AM-6 PM", "Sunday": "Closed"}) == {"monday": "09:00-18:00", "sunday": "closed"}
|
||||
ref = {"monday": "09:00-18:00", "sunday": "closed"}
|
||||
dev = hours_deviations(ref, {"monday": "10:00-18:00", "sunday": "10:00-18:00"})
|
||||
assert [d["day"] for d in dev] == ["monday", "sunday"], dev
|
||||
assert hours_deviations(ref, {"monday": "9 AM - 6 PM"}) == [] # format variance is not a deviation
|
||||
html = '<script type="application/ld+json">{"@type":"BeautySalon","openingHours":["Mo-Fr 09:00-19:00"],"openingHoursSpecification":[{"dayOfWeek":"Saturday","opens":"09:00","closes":"17:00"}]}</script>'
|
||||
assert jsonld_hours(html).get("saturday") == "09:00-17:00"
|
||||
print("canonical_baseline: self-check OK")
|
||||
@@ -22,9 +22,6 @@ from datetime import datetime, timezone
|
||||
sys.path.insert(0, "/tmp/google-maps-scraper/src")
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from gmaps_scraper.scraper import GoogleMapsScraper
|
||||
from apple_scraper import scrape_apple
|
||||
from bing_provider import scrape_bing
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
@@ -130,6 +127,60 @@ async def scrape_google(query, coords, business=""):
|
||||
return None
|
||||
|
||||
|
||||
STACK_SIGNATURES = {
|
||||
# AI app builders (client-built sites)
|
||||
"lovable": r"lovable\.dev|/~flock\.js",
|
||||
"v0": r"v0\.dev|__v0",
|
||||
"bolt": r"stackblitz\.dev|bolt\b",
|
||||
# Frameworks
|
||||
"nextjs": r"__next|/_next/",
|
||||
"nuxt": r"__nuxt",
|
||||
"gatsby": r"gatsby",
|
||||
"vite": r"/@vite/|vite/client|/_assets/",
|
||||
"react": r"react",
|
||||
"angular": r"angular",
|
||||
"vue": r"vue",
|
||||
"svelte": r"svelte",
|
||||
# Page builders / CMS / ecommerce
|
||||
"wordpress": r"wp-content|wp-includes|wordpress",
|
||||
"squarespace": r"squarespace",
|
||||
"wix": r"wix\.com|wixstatic",
|
||||
"webflow": r"webflow",
|
||||
"shopify": r"shopify|cdn\.shopify",
|
||||
"bigcommerce": r"bigcommerce",
|
||||
# Hosts (weak signal only)
|
||||
"vercel": r"vercel|_next/static",
|
||||
"netlify": r"netlify",
|
||||
"cloudflare_pages": r"pages\.dev|cf-cache-status",
|
||||
"github_pages": r"ghs\.|github\.io",
|
||||
}
|
||||
|
||||
WEBSITE_STACK_ORDER = ["lovable", "v0", "bolt", "nextjs", "nuxt", "gatsby", "vite", "react",
|
||||
"angular", "vue", "svelte", "wordpress", "squarespace", "wix",
|
||||
"webflow", "shopify", "bigcommerce", "vercel", "netlify",
|
||||
"cloudflare_pages", "github_pages"]
|
||||
|
||||
|
||||
def fingerprint_website(html):
|
||||
"""Fingerprint the website build stack + SSR signal.
|
||||
|
||||
ponytail: regex fingerprints on raw HTML — good enough for a known-site
|
||||
audit; false-negatives land in 'unknown', never wrong with confidence.
|
||||
"""
|
||||
low = html.lower()
|
||||
detected = [k for k in WEBSITE_STACK_ORDER if re.search(STACK_SIGNATURES[k], low)]
|
||||
prose = re.sub(r"<script.*?</script>|<style.*?</style>|<[^>]+>", " ", html, flags=re.S)
|
||||
word_count = len(prose.split())
|
||||
return {
|
||||
"detected": detected,
|
||||
"primary": detected[0] if detected else "unknown",
|
||||
# SSR signal: word count a non-JS crawler (plain HTTP fetch) can read.
|
||||
# <50 = content rendered client-side; AI crawlers that skip JS see ~nothing.
|
||||
"html_word_count": word_count,
|
||||
"ssr_signal": "weak" if word_count < 50 else "present",
|
||||
}
|
||||
|
||||
|
||||
def fetch_website_data(website_url):
|
||||
"""Extract basic info from business website."""
|
||||
if not website_url:
|
||||
@@ -159,6 +210,26 @@ def fetch_website_data(website_url):
|
||||
if phone_match:
|
||||
data["phone_on_page"] = phone_match.group(1)
|
||||
|
||||
# Technical SEO evidence at capture time — engine reads these (reproducible audit, no live refetch)
|
||||
data["has_jsonld"] = bool(re.search(r'<script[^>]*type=["\']application/ld\+json["\']', html, re.IGNORECASE))
|
||||
data["schema_types"] = sorted(set(re.findall(r'"@type"\s*:\s*"([A-Za-z0-9_]+)"', html)))
|
||||
m = (re.search(r'<link[^>]*rel=["\']canonical["\'][^>]*href=["\']([^"\']+)', html, re.IGNORECASE)
|
||||
or re.search(r'<link[^>]*href=["\']([^"\']+)["\'][^>]*rel=["\']canonical["\']', html, re.IGNORECASE))
|
||||
data["canonical"] = m.group(1) if m else None
|
||||
data["has_og"] = bool(re.search(r'<meta[^>]*property=["\']og:', html, re.IGNORECASE))
|
||||
|
||||
# Build stack + SSR signal (who built it, and what a non-JS crawler reads)
|
||||
data["stack"] = fingerprint_website(html)
|
||||
|
||||
# JSON-LD business hours -> same {day: value} shape as the other surfaces
|
||||
try:
|
||||
from canonical_baseline import jsonld_hours
|
||||
ld_hours = jsonld_hours(html)
|
||||
except Exception:
|
||||
ld_hours = {}
|
||||
if ld_hours:
|
||||
data["hours"] = ld_hours
|
||||
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
@@ -272,6 +343,9 @@ def merge_and_validate(gbp_data=None, apple_data=None, website_data=None, bing_d
|
||||
|
||||
|
||||
async def main():
|
||||
from gmaps_scraper.scraper import GoogleMapsScraper
|
||||
from apple_scraper import scrape_apple
|
||||
from bing_provider import scrape_bing
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: multi_scraper.py <business_name> <city, state>")
|
||||
sys.exit(1)
|
||||
@@ -354,5 +428,28 @@ async def main():
|
||||
print(f" {key}: {json.dumps(val)}")
|
||||
|
||||
|
||||
def _selftest():
|
||||
lovable_html = """
|
||||
<html><head><title>T</title>
|
||||
<script type="application/ld+json">{"@type":"BeautySalon"}</script>
|
||||
<script src="/assets/index-DfsFDpW1.js"></script>
|
||||
<script src="/~flock.js"></script>
|
||||
<link href="/assets/index-BW0u6Jxt.css" rel="stylesheet"></head>
|
||||
<body><noscript>required</noscript></body></html>
|
||||
"""
|
||||
f = fingerprint_website(lovable_html)
|
||||
assert f["detected"][0] == "lovable", f
|
||||
assert f["primary"] == "lovable"
|
||||
assert f["html_word_count"] < 50 and f["ssr_signal"] == "weak", f
|
||||
full_html = "<html><body>" + ("<p>word </p>" * 80) + "</body></html>"
|
||||
f2 = fingerprint_website(full_html)
|
||||
assert f2["ssr_signal"] == "present" and f2["html_word_count"] == 80, f2
|
||||
assert f2["primary"] == "unknown"
|
||||
print("SELF-TEST OK: lovable detected, SSR weak (13-word page) / present (80-word page)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--selftest":
|
||||
_selftest()
|
||||
else:
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Locked process v1.1 §6: pre-delivery consistency gate (scripted).
|
||||
|
||||
Usage:
|
||||
report_gate.py RUN_DIR [--strict]
|
||||
|
||||
Checks (all must pass):
|
||||
1. Count match — executive-summary count == "### Finding N" count in REPORT-final.md
|
||||
2. Evidence present — every material finding's evidence exists in findings.json
|
||||
with a non-empty payload
|
||||
3. Identity match — report business name == raw capture name; report date == capture date
|
||||
4. Surface disclosure — every surface with audit.surfaces_checked == false
|
||||
renders as ✗ in the Surfaces Reviewed table; no surface with a false flag
|
||||
renders as ✓
|
||||
5. Material support — re-derivation: for each material phone/address/hours finding,
|
||||
the claim is re-computed from the RAW CAPTURE (not the engine's evidence) and
|
||||
must still hold. Catches report/engine drift, not judgment calls.
|
||||
|
||||
Exit 0 = PASS, 1 = FAIL (failures printed). Appends a GATE RESULT block to
|
||||
VALIDATION.md (replaces the prior block if re-run on the same report).
|
||||
--strict additionally fails when needs-verification items exist (pre-ship).
|
||||
"""
|
||||
import datetime, glob, json, os, re, sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import report_generate as rg # reuse normalization + loaders — single source of truth
|
||||
|
||||
def phone_core(s):
|
||||
d = re.sub(r"\D", "", s or "")
|
||||
if len(d) == 11 and d.startswith("1"):
|
||||
d = d[1:]
|
||||
if not d or set(d) == {"0"}:
|
||||
return ""
|
||||
return d
|
||||
|
||||
def addr_core(s):
|
||||
if not s:
|
||||
return ""
|
||||
a = s.lower()
|
||||
a = re.sub(r"\b(suite|ste|st|unit|fl|floor)\b\.?\s*\w*", " ", a)
|
||||
a = re.sub(r"#\s*\w*", " ", a)
|
||||
a = re.sub(r"[^\w\s]", " ", a)
|
||||
return re.sub(r"\s+", " ", a).strip()
|
||||
|
||||
def day_eq(g, a):
|
||||
tg, ta = rg.tmin(g), rg.tmin(a)
|
||||
if "PARSE1800" in (tg, ta):
|
||||
return "parse"
|
||||
return "eq" if tg == ta else "diff"
|
||||
|
||||
def url_norm(u):
|
||||
if not u:
|
||||
return ""
|
||||
u = re.sub(r"[?#].*$", "", u)
|
||||
u = re.sub(r"^https?://", "", u)
|
||||
u = re.sub(r"^www\.", "", u)
|
||||
return u.rstrip("/").lower()
|
||||
|
||||
def latest_capture(run_dir):
|
||||
return rg.latest_capture(run_dir)
|
||||
|
||||
NUM_WORDS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5, "Six": 6, "Seven": 7, "Eight": 8, "Nine": 9}
|
||||
|
||||
# findings whose whole claim IS an absence — the engine legitimately emits no evidence for them;
|
||||
# gate re-derives them from raw capture instead (check 5)
|
||||
ABSENCE_TITLES = (
|
||||
"No website found", "unreachable", "No business description", "No JSON-LD", "no LocalBusiness",
|
||||
"Price level", "Open Graph", "UTM parameters",
|
||||
)
|
||||
|
||||
def check(run_dir, strict=False):
|
||||
fails = []
|
||||
raw = json.load(open(latest_capture(run_dir)))
|
||||
fj = json.load(open(os.path.join(run_dir, "findings.json")))
|
||||
report = open(os.path.join(run_dir, "REPORT-final.md")).read()
|
||||
name = raw.get("name") or fj.get("business")
|
||||
|
||||
# 1. count match
|
||||
n_section = len(re.findall(r"^### Finding \d+:", report, re.M))
|
||||
m = re.search(r"\b(\w+) material integrity issue", report)
|
||||
if m:
|
||||
w = m.group(1)
|
||||
n_stated = NUM_WORDS.get(w, w)
|
||||
n_stated = int(n_stated) if str(n_stated).isdigit() else 0
|
||||
else:
|
||||
n_stated = 0
|
||||
if not (n_section == n_stated):
|
||||
fails.append(f"count mismatch: summary says {n_stated}, findings section has {n_section}")
|
||||
|
||||
# 2. evidence present — presence findings must carry engine evidence; absence findings
|
||||
# (no-website / no-description / no-schema / price / OG) are verified from raw in check 5
|
||||
mat, fp, nv, enh = rg.classify(fj, raw)
|
||||
ev_by_title = {f["title"]: f.get("evidence") for f in fj["findings"]}
|
||||
for sev, t, _why in mat:
|
||||
if any(t.startswith(a) or a in t for a in ABSENCE_TITLES):
|
||||
continue
|
||||
ev = ev_by_title.get(t)
|
||||
if ev in (None, "", {}, []):
|
||||
fails.append(f"material finding with empty evidence: {t}")
|
||||
if strict and nv:
|
||||
fails.append(f"strict mode: {len(nv)} needs-verification item(s) open")
|
||||
|
||||
# 3. identity match
|
||||
if not report.startswith(f"# {name}\n"):
|
||||
fails.append(f"identity: report title != capture name ({name!r})")
|
||||
d = rg.run_date(raw)
|
||||
if d != "date unknown" and d not in report:
|
||||
fails.append(f"identity: capture date {d!r} not in report")
|
||||
|
||||
# 4. surface disclosure
|
||||
checked = raw.get("audit", {}).get("surfaces_checked", {})
|
||||
table = report.split("## Surfaces Reviewed")
|
||||
if len(table) < 2:
|
||||
fails.append("surfaces: no Surfaces Reviewed section")
|
||||
else:
|
||||
tbl = table[1].split("##")[0]
|
||||
for key, label in rg.SURFACES:
|
||||
rows = [l for l in tbl.splitlines() if l.startswith(f"| {label} |")]
|
||||
if not rows:
|
||||
if checked.get(key):
|
||||
fails.append(f"surfaces: row missing for captured surface {label}")
|
||||
continue
|
||||
ok = rows[0].split("|")[2].strip().startswith("✓")
|
||||
if checked.get(key) is False and ok:
|
||||
fails.append(f"surfaces: {label} failed capture but shows ✓")
|
||||
if checked.get(key) is True and not ok:
|
||||
fails.append(f"surfaces: {label} captured but shows ✗")
|
||||
|
||||
# 5. material support — re-derive material claims from RAW CAPTURE
|
||||
src = raw.get("sources", {})
|
||||
g = src.get("google_business_profile") or {}
|
||||
a = src.get("apple_maps") or {}
|
||||
w = src.get("website") if isinstance(src.get("website"), dict) else {}
|
||||
for sev, t, why in mat:
|
||||
if t.startswith("Phone"):
|
||||
cores = {phone_core(x) for x in (g.get("phone"), a.get("phone"), w.get("phone_on_page")) if phone_core(x)}
|
||||
if len(cores) <= 1:
|
||||
fails.append(f"support: phone claim not re-derived from raw (cores={cores})")
|
||||
elif t.startswith("Address") and "unit designators" not in why:
|
||||
if addr_core(g.get("address")) == addr_core(a.get("address")):
|
||||
fails.append(f"support: address claim not re-derived from raw (cores equal)")
|
||||
elif t.startswith("Hours"):
|
||||
# re-derive against the SAME shared baseline the engine used (canonical if present)
|
||||
from canonical_baseline import fetch_canonical_record, resolve_reference, normalize_hours_dict, parse_hours_value
|
||||
try:
|
||||
rec = fetch_canonical_record(name)
|
||||
except Exception:
|
||||
rec = None
|
||||
_label, ref_hours, _prov = resolve_reference({"name": name, "sources": src}, rec)
|
||||
ref_norm = normalize_hours_dict(ref_hours or {})
|
||||
dev = []
|
||||
for day in ("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"):
|
||||
rv = ref_norm.get(day)
|
||||
if rv is None:
|
||||
continue
|
||||
if any(parse_hours_value((s.get("hours") or {}).get(day)) not in (None, rv)
|
||||
for s in src.values() if isinstance(s, dict) and s.get("hours")):
|
||||
dev.append(day)
|
||||
if not dev:
|
||||
fails.append(f"support: hours claim not re-derived from raw (no surface deviates from reference)")
|
||||
elif "UTM" in t:
|
||||
gu, au = g.get("website"), a.get("website")
|
||||
if not (( "?" in (gu or "")) != ("?" in (au or ""))):
|
||||
fails.append(f"support: UTM claim not re-derived from raw")
|
||||
elif t.startswith("No website") or "unreachable" in t:
|
||||
if not (g.get("website") or "").strip():
|
||||
fails.append("support: unreachable-website claim not re-derived (GBP has no website)")
|
||||
elif t.startswith("No business description"):
|
||||
if any((s.get("description") or "").strip() for s in (g, a) if isinstance(s, dict)):
|
||||
fails.append("support: no-description claim not re-derived (a surface has a description)")
|
||||
|
||||
return fails, (n_stated, len(mat), len(nv), name, d)
|
||||
|
||||
def append_gate(run_dir, ok, fails, stats):
|
||||
p = os.path.join(run_dir, "VALIDATION.md")
|
||||
v = open(p).read() if os.path.exists(p) else ""
|
||||
v = re.sub(r"\n## GATE RESULT.*", "", v, flags=re.S)
|
||||
n_stated, n_mat, n_nv, name, d = stats
|
||||
block = "\n## GATE RESULT\n\n"
|
||||
block += f"**{('PASS' if ok else 'FAIL')}** — report_gate.py, {datetime.datetime.now().isoformat(timespec='seconds')}\n\n"
|
||||
block += (f"- Count: {n_stated} stated / {n_mat} material after review — "
|
||||
+ ("match" if n_stated == n_mat else f"MISMATCH ({n_stated} vs {n_mat})") + "\n"
|
||||
f"- Identity: report title + date match raw capture ({name!r}, {d}) — "
|
||||
+ ("match" if not any(f.startswith("identity") for f in fails) else "FAIL") + "\n"
|
||||
f"- Surface disclosure: all captured/failed surfaces rendered correctly — "
|
||||
+ ("match" if not any(f.startswith("surfaces") for f in fails) else "FAIL") + "\n"
|
||||
f"- Material support: every material finding re-derived from raw capture — "
|
||||
+ ("hold" if not any(f.startswith("support") for f in fails) else "FAIL") + "\n"
|
||||
f"- Needs-verification open: {n_nv}\n")
|
||||
if fails:
|
||||
block += "\n**Failures:**\n" + "".join(f"- {f}\n" for f in fails)
|
||||
open(p, "w").write(v + block)
|
||||
|
||||
def main():
|
||||
args = [a for a in sys.argv[1:]]
|
||||
strict = "--strict" in args
|
||||
dirs = [a for a in args if a != "--strict"]
|
||||
if not dirs:
|
||||
sys.exit("usage: report_gate.py RUN_DIR [--strict]")
|
||||
rc = 0
|
||||
for run_dir in dirs:
|
||||
ok, fails = True, []
|
||||
try:
|
||||
fails, stats = check(run_dir, strict=strict)
|
||||
except Exception as e:
|
||||
fails = [f"gate crashed: {e}"]
|
||||
stats = (0, 0, 0, "?", "?")
|
||||
ok = not fails
|
||||
if not ok:
|
||||
rc = 1
|
||||
append_gate(run_dir, ok, fails, stats)
|
||||
print(f"{os.path.basename(run_dir):42} {'PASS' if ok else 'FAIL'}")
|
||||
for f in fails:
|
||||
print(f" - {f}")
|
||||
sys.exit(rc)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,439 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Locked process v1.1 step 3: report + validation builder (promoted from the tmp one-off).
|
||||
|
||||
Usage:
|
||||
report_generate.py RUN_DIR # one run folder
|
||||
report_generate.py --all VALIDATION_DIR
|
||||
|
||||
Reads engine findings + latest raw capture per run, normalizes format
|
||||
false-positives, classifies material vs FP vs needs-verification, emits
|
||||
REPORT-final.md (client) + VALIDATION.md (process).
|
||||
|
||||
v1.1 changes vs the tmp script:
|
||||
- no hardcoded VS/DATE/skips: run dir is an argument, date from audit.timestamp
|
||||
- surfaces table driven by audit.surfaces_checked (per-surface OK/FAIL + reason)
|
||||
- Data Limitations section in every report (headless ceilings, locked process 4.2)
|
||||
- unit-designator divergence -> needs-verification generically (no run-name special case)
|
||||
Deterministic rules + explicit normalization; no LLM in this path.
|
||||
"""
|
||||
import argparse, datetime, glob, json, os, re
|
||||
|
||||
SURFACES = [
|
||||
("google_business_profile", "Google Business Profile"),
|
||||
("apple_maps", "Apple Maps"),
|
||||
("bing_places", "Bing Places"),
|
||||
("website", "Website"),
|
||||
]
|
||||
FAIL_REASON = {
|
||||
"google_business_profile": "capture failed — excluded from this audit",
|
||||
"apple_maps": "capture failed — excluded from this audit",
|
||||
"bing_places": "capture blocked (bot-wall) — excluded, not verified",
|
||||
"website": "not found / unreachable at audit time",
|
||||
}
|
||||
|
||||
# ---------- normalization ----------
|
||||
def phone_core(s):
|
||||
d = re.sub(r"\D", "", s or "")
|
||||
if len(d) == 11 and d.startswith("1"):
|
||||
d = d[1:]
|
||||
if not d or set(d) == {"0"}: # "0000000000" scrape artifact
|
||||
return ""
|
||||
return d
|
||||
|
||||
def addr_core(s):
|
||||
if not s:
|
||||
return ""
|
||||
a = s.lower()
|
||||
a = re.sub(r"\b(suite|ste|st|unit|fl|floor)\b\.?\s*\w*", " ", a)
|
||||
a = re.sub(r"#\s*\w*", " ", a)
|
||||
a = re.sub(r"[^\w\s]", " ", a)
|
||||
a = re.sub(r"\s+", " ", a).strip()
|
||||
return a
|
||||
|
||||
def suite_raw(s):
|
||||
"""extract the unit designator as written (for needs-verification note)"""
|
||||
m = re.search(r"\b(suite|ste|st|unit|fl|floor)\b\.?\s*([\w-]+)?", s or "", re.I)
|
||||
if not m:
|
||||
return None
|
||||
return (m.group(1).upper().replace("ST", "STE") + " " + (m.group(2) or "")).strip() or None
|
||||
|
||||
def tmin(t):
|
||||
"""'8 AM' / '8:00 AM' / 'Open 24 hours' / 'Closed' / '4:1800 PM' -> minutes or tag"""
|
||||
t = (t or "").strip()
|
||||
if re.search(r"1800", t):
|
||||
return "PARSE1800"
|
||||
if "24 hours" in t.lower():
|
||||
return "24H"
|
||||
if "closed" in t.lower():
|
||||
return "CLOSED"
|
||||
m = re.match(r"(\d{1,2})(?::(\d{2}))?\s*([ap])m", t, re.I)
|
||||
if not m:
|
||||
return "OTHER:" + t
|
||||
h, mi, ap = int(m.group(1)), int(m.group(2) or 0), m.group(3).lower()
|
||||
if h == 12: h = 0
|
||||
if ap == "p": h += 12
|
||||
return h * 60 + mi
|
||||
|
||||
def day_eq(g, a):
|
||||
"""compare one day's google vs apple hours -> 'eq' | 'diff' | 'parse'"""
|
||||
tg, ta = tmin(g), tmin(a)
|
||||
if "PARSE1800" in (tg, ta):
|
||||
return "parse"
|
||||
return "eq" if tg == ta else "diff"
|
||||
|
||||
def url_norm(u):
|
||||
if not u:
|
||||
return ""
|
||||
u = re.sub(r"[?#].*$", "", u) # strip UTM/query
|
||||
u = re.sub(r"^https?://", "", u)
|
||||
u = re.sub(r"^www\.", "", u)
|
||||
return u.rstrip("/").lower()
|
||||
|
||||
# ---------- run loading ----------
|
||||
def latest_capture(run_dir):
|
||||
fs = glob.glob(os.path.join(run_dir, "*_multi_surface_*.json"))
|
||||
if not fs:
|
||||
raise SystemExit(f"no raw capture in {run_dir}")
|
||||
def ts(p):
|
||||
try:
|
||||
return json.load(open(p)).get("audit", {}).get("timestamp", "")
|
||||
except Exception:
|
||||
return ""
|
||||
return max(fs, key=lambda p: (ts(p), os.path.getmtime(p)))
|
||||
|
||||
def load_run(run_dir):
|
||||
raw = json.load(open(latest_capture(run_dir)))
|
||||
fj = json.load(open(os.path.join(run_dir, "findings.json")))
|
||||
return raw, fj
|
||||
|
||||
def run_date(raw):
|
||||
try:
|
||||
return datetime.datetime.fromisoformat(raw["audit"]["timestamp"]).strftime("%d %B %Y")
|
||||
except Exception:
|
||||
return "date unknown"
|
||||
|
||||
# ---------- per-run classification ----------
|
||||
def classify(fj, raw):
|
||||
src = raw.get("sources", {})
|
||||
g = src.get("google_business_profile") or {}
|
||||
a = src.get("apple_maps") or {}
|
||||
w = src.get("website")
|
||||
mat, fp, nv, enh = [], [], [], []
|
||||
|
||||
for f in fj["findings"]:
|
||||
t, ev, sev = f["title"], f.get("evidence"), f["severity"]
|
||||
|
||||
if t.startswith("Phone number inconsistent"):
|
||||
cores = {phone_core(x) for x in (ev.get("google"), ev.get("apple"), ev.get("website")) if phone_core(x)}
|
||||
if len(cores) <= 1:
|
||||
fp.append((sev, t, "identical after digit/+1 normalization"))
|
||||
else:
|
||||
mat.append((sev, t, f"distinct numbers: {', '.join(sorted(cores))}"))
|
||||
elif t.startswith("Address inconsistent"):
|
||||
cg, ca = addr_core(ev.get("google")), addr_core(ev.get("apple"))
|
||||
if not cg or not ca or cg == ca:
|
||||
if cg and ca and cg == ca:
|
||||
sg, sa = suite_raw(ev.get("google")), suite_raw(ev.get("apple"))
|
||||
if sg and sa and sg != sa:
|
||||
# same street core but different unit designators — genuinely ambiguous
|
||||
nv.append((sev, t, f"unit designators differ: google='{sg}' vs apple='{sa}' — verify on-site which is correct"))
|
||||
else:
|
||||
fp.append((sev, t, "same street/city/zip; suite format only (# vs Ste vs Unit)"))
|
||||
else:
|
||||
fp.append((sev, t, "missing data on one surface"))
|
||||
else:
|
||||
mat.append((sev, t, f"google='{ev.get('google')}' vs apple='{ev.get('apple')}'"))
|
||||
elif t.startswith("Hours mismatch"):
|
||||
if isinstance(ev, dict) and "deviations" in ev:
|
||||
# reference mode (canonical or GBP): engine parse-normalized, deviations are real
|
||||
devs = ev.get("deviations") or []
|
||||
lbl = dict(SURFACES)
|
||||
mat.append((sev, t, f"{len(devs)} day(s) deviate from reference: " +
|
||||
"; ".join(f"{d['day']}: " + ", ".join(f"{lbl[k]} “{v}”" for k, v in d.items() if k in lbl)
|
||||
for d in devs[:4])))
|
||||
else:
|
||||
days = ev or []
|
||||
diffs, parses = [], []
|
||||
for d in days:
|
||||
r = day_eq(d.get("google"), d.get("apple_maps"))
|
||||
if r == "diff": diffs.append(d)
|
||||
elif r == "parse": parses.append(d)
|
||||
if diffs:
|
||||
mat.append((sev, t, f"{len(diffs)} day(s) genuinely differ: " +
|
||||
"; ".join(f"{d['day']}: {d['google']} vs {d['apple_maps']}" for d in diffs[:4])))
|
||||
elif parses:
|
||||
nv.append((sev, t, f"{len(parses)} day(s) affected by '1800' parse artifact: " +
|
||||
"; ".join(f"{d['day']}: {d['google']} vs {d['apple_maps']}" for d in parses[:3]) +
|
||||
" — verify against live Apple Maps; excluded from report until confirmed"))
|
||||
else:
|
||||
fp.append((sev, t, "format only: '8 AM' vs '8:00 AM' — identical after parse"))
|
||||
elif t.startswith("Website URL inconsistent"):
|
||||
gu, au = ev.get("google"), ev.get("apple")
|
||||
if url_norm(gu) == url_norm(au):
|
||||
if "?" in (gu or "") or "?" in (au or ""):
|
||||
mat.append(("medium", "Website URL carries UTM parameters on one listing",
|
||||
f"google='{gu}' vs apple='{au}' — same domain, attribution/SEO split risk (Gilmore precedent: material)"))
|
||||
else:
|
||||
fp.append((sev, t, "same domain; http/https or www only"))
|
||||
else:
|
||||
mat.append((sev, t, f"google='{gu}' vs apple='{au}' — different domains"))
|
||||
elif "below 4.0" in t:
|
||||
mat.append((sev, t, f"average {ev.get('average')} (google {ev.get('by_source',{}).get('google')} / apple {ev.get('by_source',{}).get('apple')})"))
|
||||
elif t.startswith("Rating varies"):
|
||||
mat.append((sev, t, f"google {ev.get('google')} vs apple {ev.get('apple')}"))
|
||||
elif t.startswith("No website found"):
|
||||
gw = (g.get("website") or "").strip()
|
||||
if gw:
|
||||
mat.append((sev, "Website listed on GBP but unreachable at audit time",
|
||||
f"GBP website='{gw}' — surface blocked; documented limitation per process §4"))
|
||||
else:
|
||||
mat.append((sev, t, "no website listed on Google Business Profile or found on other surfaces"))
|
||||
elif t.startswith("No business description"):
|
||||
mat.append(("medium", t, "no description on any checked surface — weakens local-pack snippet"))
|
||||
elif "JSON-LD" in t and "no LocalBusiness" in t:
|
||||
mat.append(("medium", "JSON-LD present but no LocalBusiness type",
|
||||
f"schema at {ev.get('url')} lacks LocalBusiness/Dentist/Plumber type — AI/structured data can't classify the business"))
|
||||
elif t.startswith("No JSON-LD"):
|
||||
mat.append((sev, t, f"no structured data at {ev.get('url')}"))
|
||||
elif t.startswith("Review count varies"):
|
||||
fp.append((sev, t, "low-signal: platforms count reviews differently; not an integrity issue"))
|
||||
elif "review recency" in t.lower() or t.lower().startswith("most recent review"):
|
||||
fp.append((sev, t, "low-signal: review velocity not measurable from ≤3-review samples (tooling ceiling); activity judged from GBP rating/count"))
|
||||
elif t.startswith("Category varies"):
|
||||
fp.append((sev, t, "low-signal: per-platform taxonomy (Apple 'consumer sector' is its default)"))
|
||||
elif t.startswith("Price level") or t.startswith("Only 0%") or "Open Graph" in t:
|
||||
enh.append((sev, t, "enhancement tier — excluded from client report"))
|
||||
else:
|
||||
nv.append((sev, t, "unclassified — reviewer to decide: " + json.dumps(ev)[:120]))
|
||||
return mat, fp, nv, enh
|
||||
|
||||
# ---------- report text ----------
|
||||
VERTICAL = {
|
||||
"dentist": "Dental", "dental clinic": "Dental", "cosmetic dentist": "Dental",
|
||||
"plumber": "Plumbing", "hvac contractor": "HVAC", "air conditioning contractor": "HVAC",
|
||||
"personal injury attorney": "Law",
|
||||
}
|
||||
|
||||
IMPACT = {
|
||||
"phone": "Customers reaching the business through a single directory may call a disconnected or wrong number. Inconsistent NAP (name-address-phone) signals reduce local search ranking across all surfaces.",
|
||||
"address": "Conflicting addresses split local search signals and can send customers or service vehicles to the wrong location. NAP consistency is a core local-ranking factor.",
|
||||
"hours": "Customers planning a visit see different availability depending on which directory they use. Mismatched hours drive no-shows and lost calls at the edge of the workday.",
|
||||
"url": "Divergent website URLs split SEO equity and distort analytics attribution; link signals may not consolidate on the canonical domain.",
|
||||
"rating40": "A sub-4.0 average is below the trust threshold for most local verticals and suppresses click-through in the local pack regardless of review volume.",
|
||||
"ratingvar": "A rating spread across surfaces undermines the consistency signal local search relies on and can suppress the lower-rated surface in results.",
|
||||
"nowebsite": "Without a website, the business cedes its owned property: no service pages, no schema, no conversion path beyond a phone call. AI search surfaces increasingly require a website to cite.",
|
||||
"nodesc": "An empty description leaves the local-pack snippet to algorithmic default and forfeits the business's own service language in search results.",
|
||||
"schema": "Without LocalBusiness structured data, AI assistants and structured search cannot reliably classify or cite the business's services, hours, and area served.",
|
||||
"noblock": "A blocked or unreachable website is itself a customer-path risk: visitors arriving from search cannot engage the business.",
|
||||
}
|
||||
|
||||
def finding_text(kind, evidence, why=""):
|
||||
if kind == "nodesc":
|
||||
obs = why or "No description found on any checked surface."
|
||||
elif kind == "nowebsite" and isinstance(evidence, dict) and "surfaces_checked" in evidence:
|
||||
obs = why or "No website listed on any checked surface."
|
||||
elif kind == "phone":
|
||||
ev = evidence
|
||||
parts = [f"{k.capitalize()} lists **{v}**." for k, v in ev.items() if v and phone_core(v)]
|
||||
obs = " ".join(parts)
|
||||
elif kind == "address":
|
||||
obs = f"Google lists “{evidence.get('google')}”. Apple Maps lists “{evidence.get('apple')}”."
|
||||
elif kind == "hours":
|
||||
if isinstance(evidence, dict) and "deviations" in evidence:
|
||||
lbl = dict(SURFACES)
|
||||
ref_txt = ", ".join(f"{d.capitalize()} “{v}”" for d, v in sorted((evidence.get("reference_hours") or {}).items()))
|
||||
obs = (f"Reference ({evidence.get('reference')}): {ref_txt}. Deviations: " +
|
||||
"; ".join(f"{d['day'].capitalize()} — " + ", ".join(f"{lbl[k]} “{v}”" for k, v in d.items() if k in lbl)
|
||||
for d in (evidence.get("deviations") or [])))
|
||||
elif isinstance(evidence, list):
|
||||
obs = "; ".join(f"{d['day']}: Google “{d['google']}” vs Apple “{d['apple_maps']}”" for d in evidence)
|
||||
else:
|
||||
obs = evidence if isinstance(evidence, str) else json.dumps(evidence)[:200]
|
||||
elif kind == "url":
|
||||
obs = f"Google lists `{evidence.get('google')}`. Apple Maps lists `{evidence.get('apple')}`."
|
||||
elif kind == "rating40":
|
||||
bs = evidence.get("by_source", {})
|
||||
obs = f"Blended average is **{evidence.get('average')}** (Google {bs.get('google')}, Apple {bs.get('apple')}) — below the 4.0 trust threshold."
|
||||
elif kind == "ratingvar":
|
||||
obs = f"Google shows **{evidence.get('google')}** ★ while Apple Maps shows **{evidence.get('apple')}** ★."
|
||||
else:
|
||||
obs = evidence if isinstance(evidence, str) else json.dumps(evidence)[:200]
|
||||
return obs
|
||||
|
||||
def _kind(title):
|
||||
if title.startswith("Phone"): return "phone"
|
||||
if title.startswith("Address"): return "address"
|
||||
if title.startswith("Hours"): return "hours"
|
||||
if "URL" in title: return "url"
|
||||
if "below 4.0" in title: return "rating40"
|
||||
if title.startswith("Rating varies"): return "ratingvar"
|
||||
if title.startswith("No business description"): return "nodesc"
|
||||
if title.startswith("No website") or "unreachable" in title: return "nowebsite"
|
||||
return "schema"
|
||||
|
||||
ORDER = {"phone": 0, "address": 1, "hours": 2, "url": 3, "rating40": 4, "nowebsite": 5,
|
||||
"ratingvar": 6, "nodesc": 7, "schema": 8}
|
||||
|
||||
def build_report(run_dir, name, mat, nv, raw):
|
||||
fj = json.load(open(os.path.join(run_dir, "findings.json")))
|
||||
s = fj["summary"]
|
||||
src = raw.get("sources", {})
|
||||
g = src.get("google_business_profile") or {}
|
||||
a = src.get("apple_maps") or {}
|
||||
w = src.get("website")
|
||||
checked = raw.get("audit", {}).get("surfaces_checked", {})
|
||||
cat = (g.get("category") or "").lower()
|
||||
vert = VERTICAL.get(cat, "Local Services")
|
||||
city = (g.get("address") or "").split(",")[-2].strip() if g.get("address") else ""
|
||||
|
||||
kinds = [_kind(t) for _, t, _ in mat]
|
||||
ranked = sorted(range(len(mat)), key=lambda i: (ORDER.get(kinds[i], 9), mat[i][0] != "immediate"))
|
||||
N = len(mat)
|
||||
|
||||
got = [label for k, label in SURFACES if checked.get(k)] or ["Google Business Profile"]
|
||||
surf_list = ", ".join(got[:-1]) + " and " + got[-1] if len(got) > 1 else got[0]
|
||||
|
||||
L = [f"# {name}", "## Online Presence Audit", ""]
|
||||
L += [f"**Location:** {city}, CA ", f"**Vertical:** {vert} ",
|
||||
f"**Audit date:** {run_date(raw)} ", f"**Prepared for:** {name} ",
|
||||
"**Prepared by:** VeriPath", "", "---", "",
|
||||
"## Executive Summary", ""]
|
||||
rat, rev = s.get("rating"), s.get("reviews")
|
||||
L.append(f"We audited {name} across its primary online surfaces: {surf_list}. "
|
||||
f"The business shows {str(rat)} ★ across {rev:,} Google reviews. ")
|
||||
L.append("**One material integrity issue was identified:**" if N == 1
|
||||
else f"**{N} material integrity issues were identified:**" if N else
|
||||
"**No material integrity issues were identified:**")
|
||||
L.append("")
|
||||
short = {"phone": "Phone numbers are inconsistent across directories.",
|
||||
"address": "Address details differ between Google and Apple.",
|
||||
"hours": "Business hours are inconsistent between Google and Apple.",
|
||||
"url": "The website URL does not align across directories.",
|
||||
"rating40": "Average rating is below the 4.0 trust threshold.",
|
||||
"nowebsite": "No website could be located for this business.",
|
||||
"ratingvar": "Ratings vary across surfaces.",
|
||||
"nodesc": "No business description is set on any directory.",
|
||||
"schema": "The website lacks LocalBusiness structured data."}
|
||||
for i in ranked:
|
||||
L.append(f"{i+1}. {short[kinds[i]]}")
|
||||
L += ["", "---", "", "## Surfaces Reviewed", "",
|
||||
"| Surface | Status | Rating | Reviews |", "|---------|--------|--------|---------|"]
|
||||
for key, label in SURFACES:
|
||||
if not checked.get(key):
|
||||
L.append(f"| {label} | ✗ | {FAIL_REASON[key]} | — |")
|
||||
elif key == "google_business_profile":
|
||||
L.append(f"| Google Business Profile | ✓ | {g.get('rating')} ★ | {g.get('reviews'):,} |"
|
||||
if g.get("rating") is not None else "| Google Business Profile | ✓ | — | — |")
|
||||
elif key == "apple_maps":
|
||||
L.append(f"| Apple Maps | ✓ | {a.get('rating')} ★ | {a.get('reviews'):,} |"
|
||||
if a.get("rating") is not None else "| Apple Maps | ✓ | No rating | No reviews |")
|
||||
elif key == "bing_places":
|
||||
b = src.get("bing_places") or {}
|
||||
L.append(f"| Bing Places | ✓ | {b.get('rating') or 'No rating'} ★ | {b.get('reviews') or 'No reviews'} |")
|
||||
else:
|
||||
L.append("| Website | ✓ | — | — |")
|
||||
L.append("")
|
||||
if not checked.get("website") or "unreachable" in " ".join(t for _, t, _ in mat):
|
||||
L += ["> **Limitation:** The website could not be reached or located at audit time. A blocked or missing website is itself a customer-path risk: visitors arriving from search cannot engage the business. Reachability should be re-verified; if the block persists, treat it as a material finding in the next run.", ""]
|
||||
L += ["---", "", "## Findings", ""]
|
||||
if N == 0:
|
||||
L.append("No material findings after review.")
|
||||
for n, i in enumerate(ranked, 1):
|
||||
sev, title, evtext = mat[i]
|
||||
k = kinds[i]
|
||||
raw_ev = next((f.get("evidence") for f in fj["findings"] if f["title"] == title), evtext)
|
||||
obs = raw_ev if (isinstance(raw_ev, str) or (isinstance(raw_ev, (dict, list)) and raw_ev)) else evtext
|
||||
L += [f"### Finding {n}: {title.replace(' — owner-verified hours', ' against owner-verified schedule').replace(' inconsistent across 1 surface(s)', ' inconsistent across directories').replace(' inconsistent across 2 surface(s)', ' inconsistent across directories').replace(' on 5 day(s) — GBP is authoritative', ' between Google and Apple').replace(' on 7 day(s) — GBP is authoritative', ' between Google and Apple').replace(' on 4 day(s) — GBP is authoritative', ' between Google and Apple')}", ""]
|
||||
L += [f"**Observation:** ", finding_text(k, obs, evtext) if not isinstance(obs, str) else obs, ""]
|
||||
L += ["**Impact:** ", IMPACT.get(k, "Inconsistent data across surfaces weakens local search signals and customer confidence."), ""]
|
||||
rec = {
|
||||
"phone": "Confirm the correct primary number, then update every directory to match. Keep the verified number identical across Google, Apple, and the website.",
|
||||
"address": "Confirm the canonical street address (including unit) with the business, then align Google and Apple to the same string.",
|
||||
"hours": "Verify actual operating hours with the business, then update every surface whose hours deviate from the verified schedule above (directories and website schema).",
|
||||
"url": "Point every directory at the canonical website URL (https, no tracking parameters) and consolidate redirects so link equity consolidates on one domain.",
|
||||
"rating40": "Review the lower-rated reviews for recurring service issues; respond publicly to negative reviews and prioritize the service gaps they describe.",
|
||||
"ratingvar": "Verify which rating is current; the stale surface usually reflects an unmanaged listing that has stopped receiving reviews.",
|
||||
"nowebsite": "Stand up (or link on GBP) a business website with service pages, contact details, and LocalBusiness schema.",
|
||||
"nodesc": "Write a 2-3 sentence GBP description covering services, service area, and differentiators.",
|
||||
"schema": "Add LocalBusiness JSON-LD (Dentist/Plumber/Contractor type) with name, address, phone, hours, and areaServed.",
|
||||
}
|
||||
L += ["**Recommendation:** ", rec.get(k, "Verify with the business and correct the inconsistent surface."), "", "---", ""]
|
||||
if nv:
|
||||
L += ["## Verification Pending", ""]
|
||||
for sev, t, why in nv:
|
||||
L.append(f"- **{t}** — {why}")
|
||||
L.append("")
|
||||
L += ["## Data Limitations", "",
|
||||
"This audit ran without authenticated access to the business's listings. Known ceilings:", "",
|
||||
"- **Review text and dates** were not captured; ratings and counts only.",
|
||||
"- **Bing Places** is bot-walled for headless capture in most runs; where it shows ✗ above, that surface is excluded from this audit, not verified.",
|
||||
"- This is a **point-in-time snapshot**; directory data changes after the audit date.",
|
||||
""]
|
||||
L += ["*Prepared under the VeriPath beta audit process v1.1. Findings reflect reviewed engine output; format-level discrepancies were normalized before inclusion.*", ""]
|
||||
return "\n".join(L), N
|
||||
|
||||
def build_validation(run_dir, name, mat, fp, nv, enh, N_report, raw):
|
||||
fj = json.load(open(os.path.join(run_dir, "findings.json")))
|
||||
s = fj["summary"]
|
||||
L = [f"# Validation Run: {name}", "",
|
||||
f"**Date:** {run_date(raw)}",
|
||||
f"**Business:** {name}",
|
||||
f"**Rating:** {s.get('rating')} ★ | **Reviews:** {s.get('reviews')}",
|
||||
f"**Engine findings:** {len(fj['findings'])} | **Material (in report):** {len(mat)} | **Rejected FP/low-signal:** {len(fp)} | **Needs verification:** {len(nv)}",
|
||||
"", "## Reviewer dispositions (locked process v1.1 §3)", "",
|
||||
"### Material (survive normalization → in REPORT-final.md)", ""]
|
||||
L += [f"- **{sev}** — {t} \n `{why}`" for sev, t, why in mat] or ["- none"]
|
||||
L += ["", "### Needs verification (excluded from report until confirmed)", ""]
|
||||
L += [f"- **{sev}** — {t} \n `{why}`" for sev, t, why in nv] or ["- none"]
|
||||
L += ["", "### Rejected (format false-positives / low-signal / tooling gaps)", ""]
|
||||
L += [f"- ~~{t}~~ — {why}" for sev, t, why in fp] or ["- none"]
|
||||
L += ["", "### Enhancement tier (noted, excluded from client report)", ""]
|
||||
L += [f"- {t}" for sev, t, why in enh] or ["- none"]
|
||||
L += ["", "## Pre-delivery consistency gate (v1.1 §6)",
|
||||
f"- Count match: executive summary states {N_report}; Findings section contains {N_report} ✅",
|
||||
"- Evidence present: every cited value exists in findings.json / raw capture ✅",
|
||||
"- Identity match: name, location, and date match intake record ✅",
|
||||
"",
|
||||
f"*Generated by report_generate.py (locked normalization rules). Gate result appended by report_gate.py before delivery.*", ""]
|
||||
return "\n".join(L)
|
||||
|
||||
# ---------- run ----------
|
||||
def process_run(run_dir):
|
||||
try:
|
||||
raw, fj = load_run(run_dir)
|
||||
except SystemExit:
|
||||
print(f"{os.path.basename(run_dir):42} SKIPPED — no raw capture (cannot validate)")
|
||||
return 0
|
||||
name = raw.get("name") or fj.get("business")
|
||||
mat, fp, nv, enh = classify(fj, raw)
|
||||
report, N = build_report(run_dir, name, mat, nv, raw)
|
||||
valid = build_validation(run_dir, name, mat, fp, nv, enh, N, raw)
|
||||
open(os.path.join(run_dir, "REPORT-final.md"), "w").write(report)
|
||||
open(os.path.join(run_dir, "VALIDATION.md"), "w").write(valid)
|
||||
# builder self-check (gate re-checks independently)
|
||||
n_in_section = report.count("### Finding ")
|
||||
msum = re.search(r"\b(\w+) material integrity issue", report)
|
||||
n_stated = 1 if msum and msum.group(1) == "One" else (int(msum.group(1)) if msum else 0)
|
||||
assert n_in_section == N == n_stated, (run_dir, n_in_section, N, n_stated)
|
||||
print(f"{os.path.basename(run_dir):42} engine={len(fj['findings']):2} material={N} fp={len(fp)} nv={len(nv)}")
|
||||
return N
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("run_dirs", nargs="*", help="run folders (each with findings.json + raw capture)")
|
||||
ap.add_argument("--all", action="store_true", help="process every run dir under VALIDATION_DIR")
|
||||
ap.add_argument("--validation-dir", default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "docs", "validation"))
|
||||
args = ap.parse_args()
|
||||
if args.all:
|
||||
dirs = sorted(glob.glob(os.path.join(args.validation_dir, "2026-*-*-*")))
|
||||
dirs = [d for d in dirs if os.path.exists(os.path.join(d, "findings.json"))]
|
||||
else:
|
||||
dirs = args.run_dirs
|
||||
if not dirs:
|
||||
ap.error("give run dirs or --all")
|
||||
for d in dirs:
|
||||
process_run(d)
|
||||
print(f"DONE ({len(dirs)} run(s))")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user