@ainyc/canonry 4.61.0 → 4.61.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/aero/references/regression-playbook.md +2 -0
- package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +1 -0
- package/dist/{chunk-ZTVBTGDW.js → chunk-4FLI5VSQ.js} +2 -2
- package/dist/{chunk-QZN3J35I.js → chunk-QU62IX7K.js} +44 -18
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/{intelligence-service-RWVBWSB5.js → intelligence-service-AZDX2EBS.js} +1 -1
- package/package.json +10 -10
|
@@ -51,3 +51,5 @@ A `gbp-sync` run produces a separate family of **location-scoped** insights (`pr
|
|
|
51
51
|
| `gbp-keyword-drop` (high/medium) | A head local search term's impressions fell month-over-month | Check whether the property still ranks for the term; refresh the profile / categories. Needs ≥2 accumulated months of `gbp_keyword_monthly` history. |
|
|
52
52
|
|
|
53
53
|
These flow through the same notification + proactive wake-up path as visibility insights, so you'll see them in the post-`gbp-sync` follow-up. Dismissals are location-scoped (one location's gap can be dismissed without silencing the same gap at a sibling location).
|
|
54
|
+
|
|
55
|
+
**Calibrating `gbp-listing-discrepancy` (don't over-claim).** The Places cross-reference only sees a narrow, schema-bound amenity subset; the broader rendered hotel module (wifi, pool, room service, room rates) lives in Google Hotel Center, which the Places API can't read. Run live against a real hotel, it surfaced exactly **one** amenity (`wheelchair accessibility`). So quote the named amenities as concrete proof, but frame the discrepancy as a **floor** ("the public listing advertises at least X that your profile doesn't"), not a full inventory. The converse also holds: a thin or empty `gbp places` result is NOT evidence the public listing is bare; it means Places carries little structured data for that place. Either way the recommendation is the same and unchanged by the count: the owner controls the structured GBP attributes AI engines cite, so any amenity the profile fails to assert is a gap worth closing.
|
|
@@ -256,6 +256,7 @@ Validated against three live businesses of different types (a computer-support s
|
|
|
256
256
|
- **Impressions skew to Maps for physical-destination businesses.** The hotel pulled 7,402 desktop-maps impressions vs 2,257 desktop-search in 30 days — people find it on Maps.
|
|
257
257
|
- **Keyword thresholding scales with volume.** A busy hotel was ~89% thresholded (its head terms like `hotels`→10,939 had exact values); both small businesses were **100% thresholded** (every keyword redacted). For the typical SMB location, expect zero exact keyword values — design the UI to lead with the `<N` floor, not exact counts.
|
|
258
258
|
- **Empty lodging / place-action profiles are the norm, and the emptiness is the product.** A real operating hotel returned a lodging resource with only `{ "name": ... }` (no amenities) and zero place-action links. That gap — "AI engines have no structured amenity data or direct-booking CTA to cite" — is exactly what canonry should surface, not an error to suppress.
|
|
259
|
+
- **The Places cross-reference is a thin slice for hotels, not the full rendered listing.** Run live against the Venice Beach hotel at the Atmosphere tier, Place Details surfaced exactly one structured amenity, `wheelchair accessibility`, even though the rendered Google hotel module advertises far more (wifi, pool, room service, room rates). Those richer fields come from **Hotel Center**, which the Places API does not expose. So a thin or empty `gbp places` amenity list is NOT evidence the public listing is sparse; Places only carries a narrow, schema-bound subset (breakfast, dining, parking, pet-friendly, accessibility, restroom, family-friendly, outdoor seating, reservations). Read `gbp-listing-discrepancy` as a **floor** on the public-vs-owner gap (proof the listing advertises *at least* the named amenities), never a complete inventory. The owner-control point still stands at any size: even one amenity the profile fails to assert is a structured-data gap the operator can close.
|
|
259
260
|
|
|
260
261
|
## Troubleshooting
|
|
261
262
|
|
|
@@ -93,7 +93,7 @@ import {
|
|
|
93
93
|
runs,
|
|
94
94
|
schedules,
|
|
95
95
|
usageCounters
|
|
96
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-QU62IX7K.js";
|
|
97
97
|
import {
|
|
98
98
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
99
99
|
AGENT_PROVIDER_IDS,
|
|
@@ -5616,7 +5616,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
5616
5616
|
return result;
|
|
5617
5617
|
}
|
|
5618
5618
|
async function backfillInsightsCommand(project, opts) {
|
|
5619
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
5619
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-AZDX2EBS.js");
|
|
5620
5620
|
const config = loadConfig();
|
|
5621
5621
|
const db = createClient(config.database);
|
|
5622
5622
|
migrate(db);
|
|
@@ -31222,6 +31222,17 @@ function matchesBrandKey(candidateKey, brandKeys) {
|
|
|
31222
31222
|
var RECURRENCE_LOOKBACK_RUNS = 5;
|
|
31223
31223
|
var HISTORY_WINDOW_RUNS = Math.max(PERSISTENT_GAP_THRESHOLD, 5);
|
|
31224
31224
|
var log = createLogger("IntelligenceService");
|
|
31225
|
+
function gbpInsightSlot(typeOrSlot) {
|
|
31226
|
+
return typeOrSlot === "gbp-lodging-gap" || typeOrSlot === "gbp-listing-discrepancy" ? "lodging" : typeOrSlot;
|
|
31227
|
+
}
|
|
31228
|
+
function gbpInsightId(projectId, locationName, type) {
|
|
31229
|
+
return `${projectId}::gbp::${locationName}::${gbpInsightSlot(type)}`;
|
|
31230
|
+
}
|
|
31231
|
+
function parseGbpInsightId(id) {
|
|
31232
|
+
const parts = id.split("::");
|
|
31233
|
+
if (parts.length !== 4 || parts[1] !== "gbp") return null;
|
|
31234
|
+
return { location: parts[2], slot: gbpInsightSlot(parts[3]) };
|
|
31235
|
+
}
|
|
31225
31236
|
var IntelligenceService = class {
|
|
31226
31237
|
constructor(db) {
|
|
31227
31238
|
this.db = db;
|
|
@@ -31320,7 +31331,7 @@ var IntelligenceService = class {
|
|
|
31320
31331
|
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq30(runs.id, runId)).get();
|
|
31321
31332
|
if (!runRow) {
|
|
31322
31333
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
31323
|
-
this.persistGbpInsights(runId, projectId, []);
|
|
31334
|
+
this.persistGbpInsights(runId, projectId, [], []);
|
|
31324
31335
|
return [];
|
|
31325
31336
|
}
|
|
31326
31337
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
@@ -31333,7 +31344,7 @@ var IntelligenceService = class {
|
|
|
31333
31344
|
)).all();
|
|
31334
31345
|
if (selected.length === 0) {
|
|
31335
31346
|
log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
|
|
31336
|
-
this.persistGbpInsights(runId, projectId, []);
|
|
31347
|
+
this.persistGbpInsights(runId, projectId, [], []);
|
|
31337
31348
|
return [];
|
|
31338
31349
|
}
|
|
31339
31350
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -31343,10 +31354,11 @@ var IntelligenceService = class {
|
|
|
31343
31354
|
const drafts = analyzeGbp(signals);
|
|
31344
31355
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
31345
31356
|
const builtInsights = drafts.map((d) => ({
|
|
31346
|
-
// Stable id
|
|
31347
|
-
//
|
|
31348
|
-
//
|
|
31349
|
-
|
|
31357
|
+
// Stable, run-independent id per (project, location, slot) so each sync
|
|
31358
|
+
// REPLACES the prior insight for that slot instead of appending a per-run
|
|
31359
|
+
// copy. The lodging slot is shared by gbp-lodging-gap and its
|
|
31360
|
+
// gbp-listing-discrepancy upgrade, so the latter supersedes the former.
|
|
31361
|
+
id: gbpInsightId(projectId, d.locationName, d.type),
|
|
31350
31362
|
type: d.type,
|
|
31351
31363
|
severity: d.severity,
|
|
31352
31364
|
title: d.title,
|
|
@@ -31355,7 +31367,7 @@ var IntelligenceService = class {
|
|
|
31355
31367
|
recommendation: d.recommendation,
|
|
31356
31368
|
createdAt: now
|
|
31357
31369
|
}));
|
|
31358
|
-
this.persistGbpInsights(runId, projectId, builtInsights);
|
|
31370
|
+
this.persistGbpInsights(runId, projectId, builtInsights, signals.map((s) => s.locationName));
|
|
31359
31371
|
log.info("gbp-intelligence.analyzed", { runId, locations: selected.length, insights: builtInsights.length });
|
|
31360
31372
|
return builtInsights;
|
|
31361
31373
|
}
|
|
@@ -31416,20 +31428,34 @@ var IntelligenceService = class {
|
|
|
31416
31428
|
return { recentMonth, priorMonth, points };
|
|
31417
31429
|
}
|
|
31418
31430
|
/**
|
|
31419
|
-
* Persist GBP insights for
|
|
31420
|
-
*
|
|
31421
|
-
*
|
|
31422
|
-
*
|
|
31431
|
+
* Persist GBP insights as CURRENT STATE for the locations a sync evaluated.
|
|
31432
|
+
* GBP insights are point-in-time profile facts, not run-to-run transitions, so
|
|
31433
|
+
* each sync REPLACES the prior insights for the locations it covered — keyed by
|
|
31434
|
+
* the run-independent `${projectId}::gbp::${location}::${slot}` id — instead of
|
|
31435
|
+
* appending a fresh per-run copy. This collapses run-over-run duplicates,
|
|
31436
|
+
* supersedes `gbp-lodging-gap` with `gbp-listing-discrepancy` (shared `lodging`
|
|
31437
|
+
* slot), and clears a location's stale insight once its gap is resolved.
|
|
31438
|
+
* Dismissals are preserved by (location, slot). Locations NOT covered by this
|
|
31439
|
+
* run (e.g. a `--location`-scoped sync) are left untouched.
|
|
31423
31440
|
*/
|
|
31424
|
-
persistGbpInsights(runId, projectId, gbpInsights) {
|
|
31425
|
-
const
|
|
31426
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(eq30(insights.
|
|
31441
|
+
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
31442
|
+
const covered = new Set(coveredLocationNames);
|
|
31443
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and23(eq30(insights.projectId, projectId), eq30(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
31444
|
+
const staleIds = [];
|
|
31445
|
+
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
31427
31446
|
for (const row of existing) {
|
|
31428
|
-
|
|
31447
|
+
const parsed = parseGbpInsightId(row.id);
|
|
31448
|
+
if (!parsed) continue;
|
|
31449
|
+
if (covered.has(parsed.location)) staleIds.push(row.id);
|
|
31450
|
+
if (row.dismissed) dismissedSlots.add(`${parsed.location}::${parsed.slot}`);
|
|
31429
31451
|
}
|
|
31430
31452
|
this.db.transaction((tx) => {
|
|
31431
|
-
|
|
31453
|
+
for (const id of staleIds) {
|
|
31454
|
+
tx.delete(insights).where(eq30(insights.id, id)).run();
|
|
31455
|
+
}
|
|
31432
31456
|
for (const insight of gbpInsights) {
|
|
31457
|
+
const parsed = parseGbpInsightId(insight.id);
|
|
31458
|
+
const dismissed = parsed ? dismissedSlots.has(`${parsed.location}::${parsed.slot}`) : false;
|
|
31433
31459
|
tx.insert(insights).values({
|
|
31434
31460
|
id: insight.id,
|
|
31435
31461
|
projectId,
|
|
@@ -31441,12 +31467,12 @@ var IntelligenceService = class {
|
|
|
31441
31467
|
provider: insight.provider,
|
|
31442
31468
|
recommendation: insight.recommendation ?? null,
|
|
31443
31469
|
cause: insight.cause ?? null,
|
|
31444
|
-
dismissed
|
|
31470
|
+
dismissed,
|
|
31445
31471
|
createdAt: insight.createdAt
|
|
31446
31472
|
}).run();
|
|
31447
31473
|
}
|
|
31448
31474
|
});
|
|
31449
|
-
log.info("gbp-intelligence.persisted", { runId, insights: gbpInsights.length });
|
|
31475
|
+
log.info("gbp-intelligence.persisted", { runId, insights: gbpInsights.length, replaced: staleIds.length });
|
|
31450
31476
|
}
|
|
31451
31477
|
/**
|
|
31452
31478
|
* Analyze a single run given an explicit previous run (or null for first run).
|
package/dist/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
setTelemetrySource,
|
|
28
28
|
showFirstRunNotice,
|
|
29
29
|
trackEvent
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-4FLI5VSQ.js";
|
|
31
31
|
import {
|
|
32
32
|
CliError,
|
|
33
33
|
EXIT_SYSTEM_ERROR,
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
projects,
|
|
52
52
|
queries,
|
|
53
53
|
renderReportHtml
|
|
54
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-QU62IX7K.js";
|
|
55
55
|
import {
|
|
56
56
|
CcReleaseSyncStatuses,
|
|
57
57
|
CheckScopes,
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createServer
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-4FLI5VSQ.js";
|
|
4
4
|
import {
|
|
5
5
|
loadConfig
|
|
6
6
|
} from "./chunk-PITZUUFV.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-QU62IX7K.js";
|
|
8
8
|
import "./chunk-URPUUKLC.js";
|
|
9
9
|
export {
|
|
10
10
|
createServer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainyc/canonry",
|
|
3
|
-
"version": "4.61.
|
|
3
|
+
"version": "4.61.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
|
|
6
6
|
"license": "FSL-1.1-ALv2",
|
|
@@ -61,26 +61,26 @@
|
|
|
61
61
|
"@types/node-cron": "^3.0.11",
|
|
62
62
|
"tsup": "^8.5.1",
|
|
63
63
|
"tsx": "^4.19.0",
|
|
64
|
-
"@ainyc/canonry-config": "0.0.0",
|
|
65
|
-
"@ainyc/canonry-api-client": "0.0.0",
|
|
66
64
|
"@ainyc/canonry-api-routes": "0.0.0",
|
|
65
|
+
"@ainyc/canonry-config": "0.0.0",
|
|
66
|
+
"@ainyc/canonry-contracts": "0.0.0",
|
|
67
67
|
"@ainyc/canonry-db": "0.0.0",
|
|
68
68
|
"@ainyc/canonry-integration-bing": "0.0.0",
|
|
69
|
-
"@ainyc/canonry-contracts": "0.0.0",
|
|
70
69
|
"@ainyc/canonry-integration-cloud-run": "0.0.0",
|
|
71
|
-
"@ainyc/canonry-
|
|
72
|
-
"@ainyc/canonry-integration-google": "0.0.0",
|
|
73
|
-
"@ainyc/canonry-integration-google-places": "0.0.0",
|
|
70
|
+
"@ainyc/canonry-api-client": "0.0.0",
|
|
74
71
|
"@ainyc/canonry-integration-commoncrawl": "0.0.0",
|
|
72
|
+
"@ainyc/canonry-integration-google": "0.0.0",
|
|
73
|
+
"@ainyc/canonry-integration-google-business-profile": "0.0.0",
|
|
75
74
|
"@ainyc/canonry-integration-traffic": "0.0.0",
|
|
76
|
-
"@ainyc/canonry-
|
|
75
|
+
"@ainyc/canonry-integration-wordpress": "0.0.0",
|
|
77
76
|
"@ainyc/canonry-provider-cdp": "0.0.0",
|
|
78
77
|
"@ainyc/canonry-provider-claude": "0.0.0",
|
|
79
|
-
"@ainyc/canonry-
|
|
78
|
+
"@ainyc/canonry-intelligence": "0.0.0",
|
|
79
|
+
"@ainyc/canonry-provider-local": "0.0.0",
|
|
80
80
|
"@ainyc/canonry-provider-gemini": "0.0.0",
|
|
81
81
|
"@ainyc/canonry-provider-openai": "0.0.0",
|
|
82
82
|
"@ainyc/canonry-provider-perplexity": "0.0.0",
|
|
83
|
-
"@ainyc/canonry-
|
|
83
|
+
"@ainyc/canonry-integration-google-places": "0.0.0"
|
|
84
84
|
},
|
|
85
85
|
"scripts": {
|
|
86
86
|
"build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",
|