Run an audit¶
There are two paths: the synthetic fixture (no credentials) and a public audit (Places API key required). Start with the fixture.
Requirements¶
- Node.js 22+
- Python 3.10+
- git
- A Google Places API (New) key for live collection only
No npm packages are installed at runtime.
Path 1 — Synthetic fixture¶
This performs no network access and needs no credential.
npm run collect -- \
--fixture .opencode/skills/home-service-gbp-audit/scripts/fixtures/acme-plumbing.raw.json \
--out .audit/acme-plumbing/demo \
--run-id demo
Output:
The fixture observations are synthetic. The output is not a scored audit.
Path 2 — Public audit¶
Export the key in your shell. Do not create a tracked .env file.
Copy the public configuration and change it for the trade and market. config/*.json is gitignored except the example.
Run collection:
npm run audit -- \
--business "Example Plumbing" \
--trade plumbing \
--city "Example City, CA" \
--config config/my-market.json \
--slug example-plumbing
Or run the collector directly:
node .opencode/skills/home-service-gbp-audit/scripts/collect.mjs \
--business "Example Plumbing" \
--trade plumbing \
--city "Example City, CA" \
--state CA \
--config config/my-market.json
Public Mode uses:
- Google Places API (New) for public place data,
- a non-destructive destination inspector,
- public profile review for fields the Places API does not expose.
The Google Business Profile APIs are reserved for the future authorized/connected mode. V1 does not use them.
It never posts, replies, books, calls, texts, submits a form, or changes the profile.
If the profile has no website¶
The collector records the absence. Do not invent a destination. The website and handoff checks will be scored from the observed absence or marked N/A where there is no destination to inspect.
If a destination is unsafe or private¶
The destination inspector rejects credentials in URLs, localhost, private IPs, and link-local addresses. The run records that the destination was not inspected. Do not bypass this guard to audit a site you do not own.
Fill the worksheet¶
The collector writes audit.json with 49 pending checks. Copy it to the report package and fill it:
For each check:
- choose a score from
allowed, - write the observed evidence,
- add at least one
evidence_refssource ID, - use
verifiedorstrong_signal, - leave configured N/A checks as
applicable: false, score: null, confidence: not_applicablewith a reason.
For each cap fact, record the structured observation. Do not set a cap directly; the scorer derives it from facts. Facts must match profile, actions, and handoff or the scorer fails.
The synthetic example is a complete reference:
Do not copy its evidence into a real audit. Use the fields and structure only.
Score¶
python3 .opencode/skills/home-service-gbp-audit/scripts/score.py \
--input reports/<slug>/<run-id>/audit.filled.json \
--out reports/<slug>/<run-id>/scores.json \
--markdown reports/<slug>/<run-id>/scorecard.md \
--strict
--strict is accepted for pipeline compatibility; validation is always strict. The scorer exits non-zero and writes nothing when the audit is malformed.
Write content and render¶
Create content.json with:
- verdict,
- three leaks, each linked to a check ID or owner-data ID,
- strengths,
- fixes with check IDs and recoverable points,
- journey summaries,
- owner-data questions,
- opportunity flags,
- limitations,
- next steps.
Then:
node .opencode/skills/home-service-gbp-audit/scripts/render.mjs \
--scores reports/<slug>/<run-id>/scores.json \
--content reports/<slug>/<run-id>/content.json \
--out reports/<slug>/<run-id>/one-page.md
node .opencode/skills/home-service-gbp-audit/scripts/render.mjs --mode full \
--scores reports/<slug>/<run-id>/scores.json \
--content reports/<slug>/<run-id>/content.json \
--out reports/<slug>/<run-id>/full-report.md
The renderer rejects an unsupported check ID, a confidence mismatch, a fix that claims more points than are recoverable, or a leak with no structured linkage.
Verify¶
node .opencode/skills/home-service-gbp-audit/scripts/verify.mjs \
--run reports/<slug>/<run-id> \
--filled reports/<slug>/<run-id>/audit.filled.json \
--scores reports/<slug>/<run-id>/scores.json \
--content reports/<slug>/<run-id>/content.json \
--report reports/<slug>/<run-id>/full-report.md \
--one-page reports/<slug>/<run-id>/one-page.md \
--out reports/<slug>/<run-id>/verification.json \
--mutation --synthetic
Use --synthetic only for a synthetic or explicitly sanitized example. A real client audit must keep its reports local unless the client has approved publication.
A run is not delivered unless the verifier exits 0.
Complete the diagnostic¶
GBP is the middle layer. Use the other two published scorecards when the question moves outside the profile:
- Local Visibility Scorecard — can prospective customers discover the business across Search, Maps, AI answers, competitors, and external authority?
- Website Scorecard — can the owned website convert the demand into a lead, booking, or estimate?
The engines are independent. Do not merge the scores; use the boundary matrix in The Scorecard Family.
Re-render without rescoring¶
The stages are independent. Change content.json, then run the two render commands and verify again. Change a check score only by editing audit.filled.json, then rescore. Never hand-edit scores.json.
Clean up¶
Raw evidence and reports are local:
They are gitignored except reports/README.md. Do not force-add them.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
GOOGLE_MAPS_API_KEY is not set |
Live mode without a key | Export the key or use --fixture |
| Places request returns 403 | API not enabled, key restricted, or quota | Check the Google Cloud project and key restrictions |
facts.X conflicts with the structured observation |
Cap fact edited independently | Restate the actual observation |
evidence_refs contains unknown source |
Source ID not in sources |
Add the source or correct the ref |
one-pager word count ... outside |
Report too long or too short | Tighten prose; keep the structured tables |
| Privacy scan fails | A credential, PII, or forbidden path was added | Remove it; rotate any exposed credential |