@ainyc/canonry 4.104.0 → 4.106.0

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.
Files changed (24) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +1 -0
  2. package/assets/assets/{BacklinksPage-CixBvw--.js → BacklinksPage-CBmXc-I8.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-CjX4mQCy.js → ChartPrimitives-Sl2xFVla.js} +1 -1
  4. package/assets/assets/ProjectPage-CwPd7SMA.js +7 -0
  5. package/assets/assets/{RunRow-Bel7mWgu.js → RunRow-StjPY-W-.js} +1 -1
  6. package/assets/assets/{RunsPage-wqKWA2Ei.js → RunsPage-C4z3tX5G.js} +1 -1
  7. package/assets/assets/{SettingsPage-esOqUkW5.js → SettingsPage-DwgWiTFo.js} +1 -1
  8. package/assets/assets/{TrafficPage-V0iCV3l4.js → TrafficPage-CF0sdg6i.js} +1 -1
  9. package/assets/assets/{TrafficSourceDetailPage-DUeuOh2v.js → TrafficSourceDetailPage-9y4px0yp.js} +1 -1
  10. package/assets/assets/{arrow-left-CVZPxpW6.js → arrow-left-Clk2AKwp.js} +1 -1
  11. package/assets/assets/{extract-error-message-BWAlA7ea.js → extract-error-message-Ccitgmu-.js} +1 -1
  12. package/assets/assets/{index-avJiga1P.js → index-wC-7udKS.js} +39 -39
  13. package/assets/assets/{trash-2-CA_L6uGC.js → trash-2-CgxrJFpf.js} +1 -1
  14. package/assets/index.html +1 -1
  15. package/dist/{chunk-GIKNEXFE.js → chunk-2AWATO4I.js} +61 -25
  16. package/dist/{chunk-GNMZTR6Y.js → chunk-F3MKVMS5.js} +62 -13
  17. package/dist/{chunk-23DCY7DL.js → chunk-VGMTLUUM.js} +4 -2
  18. package/dist/{chunk-Q7FFDQU4.js → chunk-VOR5ZTUU.js} +75 -2
  19. package/dist/cli.js +32 -12
  20. package/dist/index.js +4 -4
  21. package/dist/{intelligence-service-GM3RXE2F.js → intelligence-service-JGZU27U3.js} +2 -2
  22. package/dist/mcp.js +2 -2
  23. package/package.json +11 -11
  24. package/assets/assets/ProjectPage-C09hcd6v.js +0 -7
@@ -1 +1 @@
1
- import{c}from"./index-avJiga1P.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
1
+ import{c}from"./index-wC-7udKS.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
package/assets/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-avJiga1P.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-wC-7udKS.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/vendor-tanstack-Be8JdV5M.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/vendor-radix-CQ77EO2y.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/vendor-recharts-C9EZkgbP.js">
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-23DCY7DL.js";
13
+ } from "./chunk-VGMTLUUM.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -115,7 +115,7 @@ import {
115
115
  siteAuditPages,
116
116
  siteAuditSnapshots,
117
117
  usageCounters
118
- } from "./chunk-GNMZTR6Y.js";
118
+ } from "./chunk-F3MKVMS5.js";
119
119
  import {
120
120
  AGENT_MEMORY_VALUE_MAX_BYTES,
121
121
  AGENT_PROVIDER_IDS,
@@ -147,7 +147,6 @@ import {
147
147
  agentMemoryUpsertRequestSchema,
148
148
  authInvalid,
149
149
  authRequired,
150
- buildEmbedClientConfig,
151
150
  buildRunErrorFromMessages,
152
151
  classifySkillFile,
153
152
  coerceSkillManifest,
@@ -158,6 +157,7 @@ import {
158
157
  dollarsToMicros,
159
158
  effectiveBrandNames,
160
159
  effectiveDomains,
160
+ embedClientConfigForRequest,
161
161
  factorStatusFromScore,
162
162
  frameAncestorsHeaderValue,
163
163
  hostOf,
@@ -165,18 +165,20 @@ import {
165
165
  isBrowserProvider,
166
166
  isGhostTelemetryEvent,
167
167
  isRetryableHttpError,
168
+ normalizeIdTokens,
168
169
  normalizeUrlPath,
169
170
  notFound,
170
171
  parseOriginList,
171
172
  providerError,
172
173
  resolveSnapshotRequestQueries,
174
+ serializeForInlineScript,
173
175
  serializeRunError,
174
176
  skillsClientSchema,
175
177
  splitList,
176
178
  validationError,
177
179
  winnabilityClassLabel,
178
180
  withRetry
179
- } from "./chunk-Q7FFDQU4.js";
181
+ } from "./chunk-VOR5ZTUU.js";
180
182
 
181
183
  // src/telemetry.ts
182
184
  import crypto from "crypto";
@@ -2756,26 +2758,17 @@ function resolveEmbedConfig(env, config) {
2756
2758
  const enabled = rawEnabled ? rawEnabled === "1" || rawEnabled.toLowerCase() === "true" : embed?.enabled === true;
2757
2759
  const allowedOrigins = env.CANONRY_EMBED_ORIGINS !== void 0 ? parseOriginList(env.CANONRY_EMBED_ORIGINS) : parseOriginList(embed?.allowOrigins);
2758
2760
  const rawViews = env.CANONRY_EMBED_VIEWS !== void 0 ? splitList(env.CANONRY_EMBED_VIEWS) : splitList(embed?.views);
2759
- const views = normalizeViews(rawViews);
2761
+ const views = normalizeIdTokens(rawViews);
2762
+ const rawProjectTabs = env.CANONRY_EMBED_PROJECT_TABS !== void 0 ? splitList(env.CANONRY_EMBED_PROJECT_TABS) : splitList(embed?.projectTabs);
2763
+ const projectTabs = normalizeIdTokens(rawProjectTabs);
2760
2764
  return {
2761
2765
  enabled,
2762
2766
  allowedOrigins,
2763
2767
  ...views ? { views } : {},
2768
+ ...projectTabs ? { projectTabs } : {},
2764
2769
  ...embed?.theme ? { theme: embed.theme } : {}
2765
2770
  };
2766
2771
  }
2767
- function normalizeViews(raw) {
2768
- if (raw.length === 0) return void 0;
2769
- const seen = /* @__PURE__ */ new Set();
2770
- const out = [];
2771
- for (const view of raw) {
2772
- const id = view.toLowerCase();
2773
- if (seen.has(id)) continue;
2774
- seen.add(id);
2775
- out.push(id);
2776
- }
2777
- return out.length > 0 ? out : void 0;
2778
- }
2779
2772
 
2780
2773
  // src/agent-config.ts
2781
2774
  function resolveAgentEnabled(env, config) {
@@ -5513,6 +5506,42 @@ import crypto13 from "crypto";
5513
5506
  import { and as and9, eq as eq11 } from "drizzle-orm";
5514
5507
  var log12 = createLogger("DiscoveryRun");
5515
5508
  var DEFAULT_SEED_COUNT = 30;
5509
+ var EMBED_RETRY_MAX_RETRIES = 3;
5510
+ var EMBED_RETRY_MAX_DELAY_MS = 1e4;
5511
+ var EMBED_ATTEMPT_TIMEOUT_MS = 6e4;
5512
+ function withAttemptTimeout(promise, timeoutMs, label) {
5513
+ return new Promise((resolve, reject) => {
5514
+ const timer = setTimeout(
5515
+ () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
5516
+ timeoutMs
5517
+ );
5518
+ promise.then(
5519
+ (value) => {
5520
+ clearTimeout(timer);
5521
+ resolve(value);
5522
+ },
5523
+ (err) => {
5524
+ clearTimeout(timer);
5525
+ reject(err instanceof Error ? err : new Error(String(err)));
5526
+ }
5527
+ );
5528
+ });
5529
+ }
5530
+ async function embedWithRetry(fn, opts = {}) {
5531
+ const attemptTimeoutMs = opts.attemptTimeoutMs ?? EMBED_ATTEMPT_TIMEOUT_MS;
5532
+ return withRetry(
5533
+ () => withAttemptTimeout(fn(), attemptTimeoutMs, "Discovery embedding call"),
5534
+ {
5535
+ maxRetries: opts.maxRetries ?? EMBED_RETRY_MAX_RETRIES,
5536
+ isRetryable: isRetryableHttpError,
5537
+ maxDelayMs: EMBED_RETRY_MAX_DELAY_MS,
5538
+ sleep: opts.sleep,
5539
+ onRetry: opts.onRetry ?? (({ attempt, delayMs }) => {
5540
+ log12.warn("discovery.embed.retry", { attempt, delayMs });
5541
+ })
5542
+ }
5543
+ );
5544
+ }
5516
5545
  var QUERIES_PER_INTENT_BUCKET = 6;
5517
5546
  async function executeDiscoveryRun(opts) {
5518
5547
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -5545,6 +5574,7 @@ async function executeDiscoveryRun(opts) {
5545
5574
  icpDescription: opts.icpDescription,
5546
5575
  dedupThreshold: opts.dedupThreshold,
5547
5576
  maxProbes: opts.maxProbes,
5577
+ probeConcurrency: opts.probeConcurrency,
5548
5578
  locations: opts.locations,
5549
5579
  deps
5550
5580
  });
@@ -5599,12 +5629,17 @@ function buildDefaultDeps(registry) {
5599
5629
  const fromGrounding = normalized.searchQueries ?? [];
5600
5630
  return {
5601
5631
  candidates: [...fromAnswer, ...fromGrounding],
5602
- provider: "gemini"
5632
+ provider: "gemini",
5633
+ // Session diagnostics: the raw-candidate split by source, persisted on
5634
+ // discovery_sessions so seed-quality calibration is measurable.
5635
+ fromAnswerCount: fromAnswer.length,
5636
+ fromGroundingCount: fromGrounding.length
5603
5637
  };
5604
5638
  },
5605
5639
  async embed(queries2) {
5606
- if (cfg.apiKey) {
5607
- return embedQueries(queries2, { apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
5640
+ const apiKey = cfg.apiKey;
5641
+ if (apiKey) {
5642
+ return embedWithRetry(() => embedQueries(queries2, { apiKey, baseUrl: cfg.baseUrl }));
5608
5643
  }
5609
5644
  throw new Error("Discovery currently requires a Gemini API key. Vertex-mode embeddings are not yet implemented.");
5610
5645
  },
@@ -6438,7 +6473,7 @@ function readStoredGroundingSources(rawResponse) {
6438
6473
  return result;
6439
6474
  }
6440
6475
  async function backfillInsightsCommand(project, opts) {
6441
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-GM3RXE2F.js");
6476
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-JGZU27U3.js");
6442
6477
  const config = loadConfig();
6443
6478
  const db = createClient(config.database);
6444
6479
  migrate(db);
@@ -11905,6 +11940,7 @@ async function createServer(opts) {
11905
11940
  icpDescription: input.icpDescription,
11906
11941
  dedupThreshold: input.dedupThreshold,
11907
11942
  maxProbes: input.maxProbes,
11943
+ probeConcurrency: input.probeConcurrency,
11908
11944
  locations: input.locations
11909
11945
  }).then(() => runCoordinator.onRunCompleted(input.runId, input.projectId)).catch((err) => {
11910
11946
  app.log.error({ runId: input.runId, err }, "Discovery run failed");
@@ -12235,21 +12271,21 @@ async function createServer(opts) {
12235
12271
  const assetsDir = opts.assetsDir ?? path9.join(dirname, "..", "assets");
12236
12272
  if (fs8.existsSync(assetsDir)) {
12237
12273
  const indexPath = path9.join(assetsDir, "index.html");
12238
- const injectConfig = (html) => {
12274
+ const injectConfig = (html, projectTabsOverride) => {
12239
12275
  const clientConfig = {};
12240
12276
  if (basePath) clientConfig.basePath = basePath;
12241
12277
  if (embed.enabled) {
12242
- const embedClient = buildEmbedClientConfig(embed);
12278
+ const embedClient = embedClientConfigForRequest(embed, projectTabsOverride);
12243
12279
  if (embedClient) clientConfig.embed = embedClient;
12244
12280
  }
12245
- const configScript = `<script>window.__CANONRY_CONFIG__=${JSON.stringify(clientConfig)}</script>`;
12281
+ const configScript = `<script>window.__CANONRY_CONFIG__=${serializeForInlineScript(clientConfig)}</script>`;
12246
12282
  const baseTag = `<base href="${basePath ?? "/"}">`;
12247
12283
  return html.replace("<head>", `<head>${baseTag}`).replace("</head>", `${configScript}</head>`);
12248
12284
  };
12249
12285
  const sendSpaDocument = (reply, html) => {
12250
12286
  reply.header("Cache-Control", "no-cache, must-revalidate");
12251
12287
  if (embedCsp) reply.header("Content-Security-Policy", embedCsp);
12252
- return reply.type("text/html").send(injectConfig(html));
12288
+ return reply.type("text/html").send(injectConfig(html, reply.request.headers["x-canonry-embed-tabs"]));
12253
12289
  };
12254
12290
  const fastifyStatic = await import("@fastify/static");
12255
12291
  await app.register(fastifyStatic.default, {
@@ -13,6 +13,8 @@ import {
13
13
  DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
14
14
  DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES,
15
15
  DISCOVERY_DEFAULT_DEDUP_THRESHOLD,
16
+ DISCOVERY_DEFAULT_PROBE_CONCURRENCY,
17
+ DISCOVERY_PROBE_CONCURRENCY_CAP,
16
18
  DISCOVERY_PROMOTE_COMPETITOR_CAP,
17
19
  DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS,
18
20
  DiscoveryBuckets,
@@ -171,6 +173,7 @@ import {
171
173
  keywordGenerateRequestSchema,
172
174
  latestProjectRunDtoSchema,
173
175
  locationContextSchema,
176
+ mapWithConcurrency,
174
177
  mentionStateFromAnswerMentioned,
175
178
  missingDependency,
176
179
  normalizeProjectAliases,
@@ -264,7 +267,7 @@ import {
264
267
  wordpressSchemaDeployResultDtoSchema,
265
268
  wordpressSchemaStatusResultDtoSchema,
266
269
  wordpressStatusDtoSchema
267
- } from "./chunk-Q7FFDQU4.js";
270
+ } from "./chunk-VOR5ZTUU.js";
268
271
 
269
272
  // src/intelligence-service.ts
270
273
  import { eq as eq38, desc as desc18, asc as asc6, and as and28, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
@@ -1133,6 +1136,11 @@ var discoverySessions = sqliteTable("discovery_sessions", {
1133
1136
  seedProvider: text("seed_provider"),
1134
1137
  seedCountRaw: integer("seed_count_raw"),
1135
1138
  seedCount: integer("seed_count"),
1139
+ // Diagnostics: split of the raw seed candidates by source — the model's
1140
+ // answer text vs. the grounding fan-out (issued search queries). Recorded at
1141
+ // seed time; nullable for legacy sessions. No gate/warning math reads these.
1142
+ seedFromAnswerCount: integer("seed_from_answer_count"),
1143
+ seedFromGroundingCount: integer("seed_from_grounding_count"),
1136
1144
  dedupThreshold: real("dedup_threshold"),
1137
1145
  probeCount: integer("probe_count"),
1138
1146
  citedCount: integer("cited_count"),
@@ -3370,6 +3378,18 @@ var MIGRATION_VERSIONS = [
3370
3378
  `ALTER TABLE api_keys ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE CASCADE`,
3371
3379
  `CREATE INDEX IF NOT EXISTS idx_api_keys_project ON api_keys(project_id)`
3372
3380
  ]
3381
+ },
3382
+ {
3383
+ version: 88,
3384
+ name: "discovery-session-seed-source-counts",
3385
+ statements: [
3386
+ // Diagnostics: split of raw seed candidates by source (answer text vs.
3387
+ // grounding fan-out), recorded at seed time. Nullable — legacy sessions
3388
+ // stay null. ALTER ADD COLUMN is idempotent (the runner swallows the
3389
+ // duplicate-column error on retry).
3390
+ `ALTER TABLE discovery_sessions ADD COLUMN seed_from_answer_count INTEGER`,
3391
+ `ALTER TABLE discovery_sessions ADD COLUMN seed_from_grounding_count INTEGER`
3392
+ ]
3373
3393
  }
3374
3394
  ];
3375
3395
  function rebuildBacklinkTableWithSource(tx, table) {
@@ -18025,7 +18045,7 @@ var routeCatalog = [
18025
18045
  method: "post",
18026
18046
  path: "/api/v1/projects/{name}/discover/run",
18027
18047
  summary: "Start a tracked-basket discovery session",
18028
- description: 'Kicks off a discovery session for the project. The pipeline: ICP description \u2192 Gemini grounded seed prompt \u2192 embed + cluster (cosine \u2265 0.95 by default) \u2192 pick canonical representatives \u2192 probe each canonical via Gemini grounding \u2192 classify into cited / aspirational / wasted-surface \u2192 aggregate competitor map. Returns immediately with `{ runId, sessionId, status: "running", consolidated }`; the actual work runs in the background. Poll `GET /projects/{name}/discover/sessions/{id}` until `status` is `completed` or `failed`. Concurrent/duplicate requests for the same (project, ICP) are consolidated onto a single in-flight session: the response carries `consolidated: true` and `200 OK` instead of `201`, and the request\'s `dedupThreshold` / `maxProbes` are ignored (the in-flight session keeps its original config).',
18048
+ description: 'Kicks off a discovery session for the project. The pipeline: ICP description \u2192 Gemini grounded seed prompt \u2192 embed + cluster (cosine \u2265 0.95 by default) \u2192 pick canonical representatives \u2192 probe each canonical via Gemini grounding \u2192 classify into cited / aspirational / wasted-surface \u2192 aggregate competitor map. Returns immediately with `{ runId, sessionId, status: "running", consolidated }`; the actual work runs in the background. Poll `GET /projects/{name}/discover/sessions/{id}` until `status` is `completed` or `failed`. Concurrent/duplicate requests for the same (project, ICP) are consolidated onto a single in-flight session: the response carries `consolidated: true` and `200 OK` instead of `201`, and the request\'s `dedupThreshold` / `maxProbes` / `probeConcurrency` are ignored (the in-flight session keeps its original config).',
18029
18049
  tags: ["discovery"],
18030
18050
  parameters: [nameParameter],
18031
18051
  requestBody: {
@@ -18038,6 +18058,12 @@ var routeCatalog = [
18038
18058
  icpDescription: { type: "string", description: "Free-text ICP. Required if the project does not have spec.icpDescription stored." },
18039
18059
  dedupThreshold: { type: "number", description: "Cosine similarity threshold for clustering. Defaults to 0.95." },
18040
18060
  maxProbes: { type: "integer", description: "Max canonical queries to probe in this session. Default 100, hard cap 500." },
18061
+ probeConcurrency: {
18062
+ type: "integer",
18063
+ minimum: 1,
18064
+ maximum: 8,
18065
+ description: "How many probes may run in parallel. Default 1 (strictly serial \u2014 the historical behaviour), hard cap 8. Probe rows are persisted in canonical order regardless of concurrency, so this only shortens wall-clock time."
18066
+ },
18041
18067
  locations: {
18042
18068
  type: "array",
18043
18069
  items: { type: "string" },
@@ -24244,7 +24270,7 @@ function buildAmbiguousSlugMessage(slug, pages) {
24244
24270
  }).join(", ");
24245
24271
  return `Multiple pages matched slug "${slug}". Candidates: ${candidates}.`;
24246
24272
  }
24247
- async function mapWithConcurrency(items, limit, iteratee) {
24273
+ async function mapWithConcurrency2(items, limit, iteratee) {
24248
24274
  const results = new Array(items.length);
24249
24275
  let nextIndex = 0;
24250
24276
  async function worker() {
@@ -24541,7 +24567,7 @@ function buildManualMetaAssist(siteUrl, slug, link, meta) {
24541
24567
  async function bulkSetSeoMeta(connection, entries, env) {
24542
24568
  const site = resolveEnvironment(connection, env);
24543
24569
  const { strategy, plugins } = await detectSeoWriteStrategy(connection, site.env);
24544
- const results = await mapWithConcurrency(
24570
+ const results = await mapWithConcurrency2(
24545
24571
  entries,
24546
24572
  3,
24547
24573
  async (entry) => {
@@ -24699,7 +24725,7 @@ async function deploySchema(connection, slug, schemas, env) {
24699
24725
  async function deploySchemaFromProfile(connection, profile, env) {
24700
24726
  const site = resolveEnvironment(connection, env);
24701
24727
  const slugEntries = Object.entries(profile.pages);
24702
- const results = await mapWithConcurrency(
24728
+ const results = await mapWithConcurrency2(
24703
24729
  slugEntries,
24704
24730
  3,
24705
24731
  async ([slug, entries]) => {
@@ -24721,7 +24747,7 @@ async function deploySchemaFromProfile(connection, profile, env) {
24721
24747
  async function getSchemaStatus(connection, env) {
24722
24748
  const site = resolveEnvironment(connection, env);
24723
24749
  const pages = await listPages(connection, site.env);
24724
- const details = await mapWithConcurrency(
24750
+ const details = await mapWithConcurrency2(
24725
24751
  pages,
24726
24752
  5,
24727
24753
  async (page) => getPageDetail(connection, page.slug, site.env)
@@ -24822,7 +24848,7 @@ async function runAudit(connection, env) {
24822
24848
  const site = resolveEnvironment(connection, env);
24823
24849
  const pages = await listPages(connection, site.env);
24824
24850
  const plugins = await listActivePlugins(connection, site.env);
24825
- const details = await mapWithConcurrency(
24851
+ const details = await mapWithConcurrency2(
24826
24852
  pages,
24827
24853
  5,
24828
24854
  async (page) => getPageDetail(connection, page.slug, site.env, plugins)
@@ -33792,6 +33818,7 @@ async function discoveryRoutes(app, opts) {
33792
33818
  icpDescription,
33793
33819
  dedupThreshold: parsed.data.dedupThreshold,
33794
33820
  maxProbes: parsed.data.maxProbes,
33821
+ probeConcurrency: parsed.data.probeConcurrency,
33795
33822
  locations
33796
33823
  });
33797
33824
  return reply.status(201).send({
@@ -34041,6 +34068,8 @@ function serializeSession(row) {
34041
34068
  seedProvider: row.seedProvider ?? null,
34042
34069
  seedCountRaw: row.seedCountRaw ?? null,
34043
34070
  seedCount: row.seedCount ?? null,
34071
+ seedFromAnswerCount: row.seedFromAnswerCount ?? null,
34072
+ seedFromGroundingCount: row.seedFromGroundingCount ?? null,
34044
34073
  dedupThreshold: row.dedupThreshold ?? null,
34045
34074
  probeCount: row.probeCount ?? null,
34046
34075
  citedCount: row.citedCount ?? null,
@@ -34134,6 +34163,10 @@ async function executeDiscovery(opts) {
34134
34163
  const dedupThreshold = opts.dedupThreshold ?? DISCOVERY_DEFAULT_DEDUP_THRESHOLD;
34135
34164
  const requestedMax = opts.maxProbes ?? DEFAULT_MAX_PROBES;
34136
34165
  const maxProbes = Math.min(Math.max(1, requestedMax), ABSOLUTE_MAX_PROBES);
34166
+ const probeConcurrency = Math.min(
34167
+ Math.max(1, Math.floor(opts.probeConcurrency ?? DISCOVERY_DEFAULT_PROBE_CONCURRENCY)),
34168
+ DISCOVERY_PROBE_CONCURRENCY_CAP
34169
+ );
34137
34170
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
34138
34171
  opts.db.update(discoverySessions).set({
34139
34172
  status: DiscoverySessionStatuses.seeding,
@@ -34164,12 +34197,21 @@ async function executeDiscovery(opts) {
34164
34197
  seedProvider: seedResult.provider,
34165
34198
  seedCountRaw,
34166
34199
  seedCount,
34200
+ // Seed-source diagnostics (answer text vs. grounding fan-out). Null when
34201
+ // the seed dep does not report the split. Not consumed by any gate.
34202
+ seedFromAnswerCount: seedResult.fromAnswerCount ?? null,
34203
+ seedFromGroundingCount: seedResult.fromGroundingCount ?? null,
34167
34204
  warning
34168
34205
  }).where(eq36(discoverySessions.id, opts.sessionId)).run();
34206
+ const probeResults = await mapWithConcurrency(
34207
+ probedCanonicals,
34208
+ probeConcurrency,
34209
+ (query) => opts.deps.probe({ project: opts.project, query })
34210
+ );
34169
34211
  const probeRows = [];
34170
34212
  const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
34171
- for (const query of probedCanonicals) {
34172
- const probe = await opts.deps.probe({ project: opts.project, query });
34213
+ const insertBaseMs = Date.now();
34214
+ const rowsToInsert = probeResults.map((probe, index2) => {
34173
34215
  const bucket = classifyProbeBucket({
34174
34216
  citationState: probe.citationState,
34175
34217
  citedDomains: probe.citedDomains,
@@ -34177,18 +34219,25 @@ async function executeDiscovery(opts) {
34177
34219
  });
34178
34220
  probeRows.push({ citedDomains: probe.citedDomains, bucket });
34179
34221
  buckets[bucket]++;
34180
- opts.db.insert(discoveryProbes).values({
34222
+ return {
34181
34223
  id: crypto28.randomUUID(),
34182
34224
  sessionId: opts.sessionId,
34183
34225
  projectId: opts.project.id,
34184
- query,
34226
+ query: probedCanonicals[index2],
34185
34227
  bucket,
34186
34228
  citationState: probe.citationState,
34187
34229
  answerMentioned: probe.answerMentioned,
34188
34230
  citedDomains: probe.citedDomains,
34189
34231
  rawResponse: JSON.stringify(probe.rawResponse),
34190
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
34191
- }).run();
34232
+ createdAt: new Date(insertBaseMs + index2).toISOString()
34233
+ };
34234
+ });
34235
+ if (rowsToInsert.length > 0) {
34236
+ opts.db.transaction((tx) => {
34237
+ for (const values of rowsToInsert) {
34238
+ tx.insert(discoveryProbes).values(values).run();
34239
+ }
34240
+ });
34192
34241
  }
34193
34242
  const domains = buildCompetitorMap(probeRows, opts.project).map((entry) => entry.domain);
34194
34243
  const classification = await classifyCompetitorDomains(
@@ -2,6 +2,7 @@ import {
2
2
  AGENT_MEMORY_KEY_MAX_LENGTH,
3
3
  AGENT_MEMORY_VALUE_MAX_BYTES,
4
4
  DISCOVERY_MAX_PROBES_CAP,
5
+ DISCOVERY_PROBE_CONCURRENCY_CAP,
5
6
  backlinkSourceSchema,
6
7
  competitorBatchRequestSchema,
7
8
  discoveryBucketSchema,
@@ -24,7 +25,7 @@ import {
24
25
  trafficConnectVercelRequestSchema,
25
26
  trafficConnectWordpressRequestSchema,
26
27
  trafficEventKindSchema
27
- } from "./chunk-Q7FFDQU4.js";
28
+ } from "./chunk-VOR5ZTUU.js";
28
29
 
29
30
  // src/config.ts
30
31
  import fs from "fs";
@@ -5565,7 +5566,8 @@ var discoveryRunInputSchema = z2.object({
5565
5566
  // upper bound; this just clarifies the meaning of each knob.
5566
5567
  icpDescription: z2.string().min(1).optional().describe("Free-text ICP description. If omitted, the project must already have spec.icpDescription stored."),
5567
5568
  dedupThreshold: z2.number().min(0).max(1).optional().describe("Cosine similarity threshold for clustering seed candidates. Defaults to 0.85. Lower values dedupe more aggressively."),
5568
- maxProbes: z2.number().int().positive().max(DISCOVERY_MAX_PROBES_CAP).optional().describe(`Max canonical queries to probe in this session. Default 100, hard cap ${DISCOVERY_MAX_PROBES_CAP}.`)
5569
+ maxProbes: z2.number().int().positive().max(DISCOVERY_MAX_PROBES_CAP).optional().describe(`Max canonical queries to probe in this session. Default 100, hard cap ${DISCOVERY_MAX_PROBES_CAP}.`),
5570
+ probeConcurrency: z2.number().int().min(1).max(DISCOVERY_PROBE_CONCURRENCY_CAP).optional().describe(`How many probes may run in parallel. Default 1 (strictly serial), hard cap ${DISCOVERY_PROBE_CONCURRENCY_CAP}. Probe rows are persisted in canonical order regardless of concurrency, so this only shortens wall-clock time.`)
5569
5571
  }).optional()
5570
5572
  });
5571
5573
  var discoverySessionsListInputSchema = z2.object({
@@ -1188,6 +1188,33 @@ function isRetryableHttpError(err) {
1188
1188
  return true;
1189
1189
  }
1190
1190
 
1191
+ // ../contracts/src/concurrency.ts
1192
+ async function mapWithConcurrency(items, concurrency, fn) {
1193
+ if (items.length === 0) return [];
1194
+ const limit = Math.min(Math.max(1, Math.floor(concurrency)), items.length);
1195
+ const results = new Array(items.length);
1196
+ const state = {
1197
+ nextIndex: 0,
1198
+ firstFailure: null
1199
+ };
1200
+ async function worker() {
1201
+ for (; ; ) {
1202
+ if (state.firstFailure) return;
1203
+ const index = state.nextIndex++;
1204
+ if (index >= items.length) return;
1205
+ try {
1206
+ results[index] = await fn(items[index], index);
1207
+ } catch (err) {
1208
+ state.firstFailure ??= { err };
1209
+ return;
1210
+ }
1211
+ }
1212
+ }
1213
+ await Promise.all(Array.from({ length: limit }, () => worker()));
1214
+ if (state.firstFailure) throw state.firstFailure.err;
1215
+ return results;
1216
+ }
1217
+
1191
1218
  // ../contracts/src/bing.ts
1192
1219
  import { z as z9 } from "zod";
1193
1220
  var bingConnectionDtoSchema = z9.object({
@@ -2150,6 +2177,15 @@ var discoverySessionDtoSchema = z17.object({
2150
2177
  seedProvider: z17.string().nullable().optional(),
2151
2178
  seedCountRaw: z17.number().int().nullable().optional(),
2152
2179
  seedCount: z17.number().int().nullable().optional(),
2180
+ /**
2181
+ * Diagnostics: how many raw seed candidates came from the model's answer
2182
+ * text vs. from the grounding fan-out (the search queries the engine
2183
+ * actually issued). Recorded at seed time so seed-quality calibration is
2184
+ * measurable per session. Null on legacy sessions (or when a seed dep does
2185
+ * not report the split). Purely additive — no gate or warning reads these.
2186
+ */
2187
+ seedFromAnswerCount: z17.number().int().nullable().optional(),
2188
+ seedFromGroundingCount: z17.number().int().nullable().optional(),
2153
2189
  dedupThreshold: z17.number().nullable().optional(),
2154
2190
  probeCount: z17.number().int().nullable().optional(),
2155
2191
  citedCount: z17.number().int().nullable().default(null),
@@ -2171,13 +2207,15 @@ var discoverySessionDetailDtoSchema = discoverySessionDtoSchema.extend({
2171
2207
  probes: z17.array(discoveryProbeDtoSchema).default([])
2172
2208
  });
2173
2209
  var DISCOVERY_MAX_PROBES_CAP = 500;
2210
+ var DISCOVERY_PROBE_CONCURRENCY_CAP = 8;
2211
+ var DISCOVERY_DEFAULT_PROBE_CONCURRENCY = 1;
2174
2212
  var DISCOVERY_DEFAULT_DEDUP_THRESHOLD = 0.95;
2175
2213
  var DISCOVERY_SEED_COLLAPSE_RATIO = 0.2;
2176
2214
  var DISCOVERY_SEED_COLLAPSE_MIN_RAW = 10;
2177
2215
  function seedCollapseWarning(input) {
2178
2216
  const { seedCountRaw, canonicalCount, dedupThreshold } = input;
2179
2217
  if (seedCountRaw < DISCOVERY_SEED_COLLAPSE_MIN_RAW) return null;
2180
- if (canonicalCount / seedCountRaw >= DISCOVERY_SEED_COLLAPSE_RATIO) return null;
2218
+ if (canonicalCount / seedCountRaw > DISCOVERY_SEED_COLLAPSE_RATIO) return null;
2181
2219
  const noun = canonicalCount === 1 ? "query" : "queries";
2182
2220
  return `Seed dedup collapsed ${seedCountRaw} raw candidates into ${canonicalCount} canonical ${noun} at threshold ${dedupThreshold}. Distinct intents were likely merged into one cluster; re-run with a higher --dedup-threshold.`;
2183
2221
  }
@@ -2185,6 +2223,14 @@ var discoveryRunRequestSchema = z17.object({
2185
2223
  icpDescription: z17.string().min(1).optional(),
2186
2224
  dedupThreshold: z17.number().min(0).max(1).optional(),
2187
2225
  maxProbes: z17.number().int().positive().max(DISCOVERY_MAX_PROBES_CAP).optional(),
2226
+ /**
2227
+ * How many probes may run in parallel for this session. Defaults to
2228
+ * `DISCOVERY_DEFAULT_PROBE_CONCURRENCY` (1 — strictly serial, the historical
2229
+ * behaviour) when omitted; capped at `DISCOVERY_PROBE_CONCURRENCY_CAP`.
2230
+ * Probe results are persisted in canonical order regardless of concurrency,
2231
+ * so this only changes wall-clock time, never output order.
2232
+ */
2233
+ probeConcurrency: z17.number().int().min(1).max(DISCOVERY_PROBE_CONCURRENCY_CAP).optional(),
2188
2234
  /**
2189
2235
  * Optional override of the project's location labels, constraining seed
2190
2236
  * generation to a subset of the configured service areas. Each label must
@@ -5415,9 +5461,31 @@ function buildEmbedClientConfig(resolved) {
5415
5461
  if (!resolved.enabled) return void 0;
5416
5462
  const client = { enabled: true };
5417
5463
  if (resolved.views && resolved.views.length > 0) client.views = resolved.views;
5464
+ if (resolved.projectTabs && resolved.projectTabs.length > 0) client.projectTabs = resolved.projectTabs;
5418
5465
  if (resolved.theme && Object.keys(resolved.theme).length > 0) client.theme = resolved.theme;
5419
5466
  return client;
5420
5467
  }
5468
+ function normalizeIdTokens(raw) {
5469
+ if (raw.length === 0) return void 0;
5470
+ const seen = /* @__PURE__ */ new Set();
5471
+ const out = [];
5472
+ for (const token of raw) {
5473
+ const id = token.toLowerCase();
5474
+ if (seen.has(id)) continue;
5475
+ seen.add(id);
5476
+ out.push(id);
5477
+ }
5478
+ return out.length > 0 ? out : void 0;
5479
+ }
5480
+ function embedClientConfigForRequest(resolved, projectTabsOverride) {
5481
+ const base = buildEmbedClientConfig(resolved);
5482
+ if (!base) return void 0;
5483
+ const override = normalizeIdTokens(splitList(projectTabsOverride));
5484
+ return override ? { ...base, projectTabs: override } : base;
5485
+ }
5486
+ function serializeForInlineScript(value) {
5487
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
5488
+ }
5421
5489
 
5422
5490
  // ../contracts/src/telemetry.ts
5423
5491
  var GHOST_TELEMETRY_TEST_LOCATIONS = /* @__PURE__ */ new Set(["nyc", "lax", "chi"]);
@@ -5509,6 +5577,7 @@ export {
5509
5577
  formatGbpMetricLabel,
5510
5578
  withRetry,
5511
5579
  isRetryableHttpError,
5580
+ mapWithConcurrency,
5512
5581
  bingUrlInspectionDtoSchema,
5513
5582
  bingCoverageSummaryDtoSchema,
5514
5583
  bingKeywordStatsDtoSchema,
@@ -5587,6 +5656,8 @@ export {
5587
5656
  discoverySessionDtoSchema,
5588
5657
  discoverySessionDetailDtoSchema,
5589
5658
  DISCOVERY_MAX_PROBES_CAP,
5659
+ DISCOVERY_PROBE_CONCURRENCY_CAP,
5660
+ DISCOVERY_DEFAULT_PROBE_CONCURRENCY,
5590
5661
  DISCOVERY_DEFAULT_DEDUP_THRESHOLD,
5591
5662
  seedCollapseWarning,
5592
5663
  discoveryRunRequestSchema,
@@ -5738,6 +5809,8 @@ export {
5738
5809
  splitList,
5739
5810
  parseOriginList,
5740
5811
  frameAncestorsHeaderValue,
5741
- buildEmbedClientConfig,
5812
+ normalizeIdTokens,
5813
+ embedClientConfigForRequest,
5814
+ serializeForInlineScript,
5742
5815
  isGhostTelemetryEvent
5743
5816
  };