mcp-scraper 0.44.1 → 0.44.3

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.
@@ -0,0 +1,7 @@
1
+ // src/version.ts
2
+ var PACKAGE_VERSION = "0.44.3";
3
+
4
+ export {
5
+ PACKAGE_VERSION
6
+ };
7
+ //# sourceMappingURL=chunk-32Z52OE4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["export const PACKAGE_VERSION = '0.44.3'\n"],"mappings":";AAAO,IAAM,kBAAkB;","names":[]}
@@ -86,7 +86,7 @@ import {
86
86
  renewDirectoryArtifactDownload,
87
87
  resolveDeploymentProfile,
88
88
  transcribeMediaUrl
89
- } from "./chunk-TMJVCYDL.js";
89
+ } from "./chunk-2SE4GYCS.js";
90
90
  import {
91
91
  auditImageUrls,
92
92
  auditImages,
@@ -142,7 +142,7 @@ import {
142
142
  } from "./chunk-3ZUBQQPQ.js";
143
143
  import {
144
144
  PACKAGE_VERSION
145
- } from "./chunk-2XGU5GVB.js";
145
+ } from "./chunk-32Z52OE4.js";
146
146
  import {
147
147
  abandonExtractSettlement,
148
148
  countSuccessfulPages,
@@ -11972,6 +11972,7 @@ function ensureLocalSourcebookSchema() {
11972
11972
  metadata_json TEXT NOT NULL DEFAULT '{}',
11973
11973
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
11974
11974
  )`);
11975
+ await publishEligibleEvidenceBackedLocalSourcebookRevisions(db);
11975
11976
  })().catch((error) => {
11976
11977
  schemaPromise2 = null;
11977
11978
  schemaDb2 = null;
@@ -11979,6 +11980,19 @@ function ensureLocalSourcebookSchema() {
11979
11980
  });
11980
11981
  return schemaPromise2;
11981
11982
  }
11983
+ async function publishEligibleEvidenceBackedLocalSourcebookRevisions(db = getDb()) {
11984
+ const result = await db.execute(`UPDATE local_sourcebook_submissions
11985
+ SET status = 'published', published_revision = draft_revision, updated_at = datetime('now')
11986
+ WHERE status = 'needs_review'
11987
+ AND EXISTS (
11988
+ SELECT 1 FROM local_sourcebook_revisions r
11989
+ WHERE r.submission_id = local_sourcebook_submissions.id
11990
+ AND r.revision = local_sourcebook_submissions.draft_revision
11991
+ AND r.actor_kind = 'system'
11992
+ AND r.actor_id = 'local-sourcebook-worker'
11993
+ )`);
11994
+ return Number(result.rowsAffected ?? 0);
11995
+ }
11982
11996
  function rowValue(row, key) {
11983
11997
  return row[key];
11984
11998
  }
@@ -12048,23 +12062,12 @@ async function getLocalSourcebookDraft(id, ownerUserId) {
12048
12062
  const result = await getDb().execute({ sql: `SELECT payload_json FROM local_sourcebook_revisions WHERE submission_id = ? AND revision = ?`, args: [id, submission.draftRevision] });
12049
12063
  return result.rows[0] ? parseJson2(result.rows[0].payload_json) : null;
12050
12064
  }
12051
- async function reviseLocalSourcebookDraft(id, ownerUserId, payload, baseRevision) {
12052
- const submission = await getLocalSourcebookSubmission(id, ownerUserId);
12053
- if (!submission) return null;
12054
- if (baseRevision !== void 0 && baseRevision !== submission.draftRevision) {
12055
- throw new Error(`Draft revision conflict: current revision is ${submission.draftRevision}, but the write targeted ${baseRevision}.`);
12056
- }
12057
- const revision = submission.draftRevision + 1;
12058
- await getDb().batch([
12059
- { sql: `INSERT INTO local_sourcebook_revisions (submission_id, revision, payload_json, actor_kind, actor_id) VALUES (?, ?, ?, 'owner', ?)`, args: [id, revision, JSON.stringify(payload), String(ownerUserId)] },
12060
- { sql: `UPDATE local_sourcebook_submissions SET draft_revision = ?, status = CASE WHEN status = 'published' THEN status ELSE 'needs_review' END, updated_at = datetime('now') WHERE id = ? AND owner_user_id = ?`, args: [revision, id, ownerUserId] }
12061
- ], "write");
12062
- await event(id, "draft.revised", "owner", String(ownerUserId), { revision });
12063
- return getLocalSourcebookSubmission(id, ownerUserId);
12064
- }
12065
12065
  async function queueLocalSourcebookRefresh(id, ownerUserId) {
12066
12066
  const existing = await getLocalSourcebookSubmission(id, ownerUserId);
12067
12067
  if (!existing) return null;
12068
+ if (existing.status === "rejected" || existing.status === "unpublished") {
12069
+ throw new Error(`A ${existing.status} Local Sourcebook listing cannot be refreshed by its owner.`);
12070
+ }
12068
12071
  const coverage = { ...existing.coverage, refreshRequestedAt: (/* @__PURE__ */ new Date()).toISOString() };
12069
12072
  await getDb().execute({ sql: `UPDATE local_sourcebook_submissions SET status = 'queued', coverage_json = ?, acquisition_claimed_at = NULL, acquisition_error = NULL, updated_at = datetime('now') WHERE id = ? AND owner_user_id = ?`, args: [JSON.stringify(coverage), id, ownerUserId] });
12070
12073
  await event(id, "enrichment.queued", "owner", String(ownerUserId));
@@ -12102,12 +12105,13 @@ async function completeLocalSourcebookAcquisition(id, payload, coverage, baseRev
12102
12105
  WHERE EXISTS (SELECT 1 FROM local_sourcebook_submissions WHERE id = ? AND status = 'enriching' AND draft_revision = ?)`,
12103
12106
  args: [id, revision, JSON.stringify(payload), id, expectedRevision]
12104
12107
  },
12105
- { sql: `UPDATE local_sourcebook_submissions SET draft_revision = ?, status = 'needs_review', coverage_json = ?, acquisition_claimed_at = NULL, acquisition_error = NULL, updated_at = datetime('now') WHERE id = ? AND status = 'enriching' AND draft_revision = ?`, args: [revision, JSON.stringify(coverage), id, expectedRevision] }
12108
+ { sql: `UPDATE local_sourcebook_submissions SET draft_revision = ?, published_revision = ?, status = 'published', coverage_json = ?, acquisition_claimed_at = NULL, acquisition_error = NULL, updated_at = datetime('now') WHERE id = ? AND status = 'enriching' AND draft_revision = ?`, args: [revision, revision, JSON.stringify(coverage), id, expectedRevision] }
12106
12109
  ], "write");
12107
12110
  if (Number(results[0]?.rowsAffected ?? 0) !== 1 || Number(results[1]?.rowsAffected ?? 0) !== 1) {
12108
12111
  throw new Error(`Local Sourcebook acquisition revision conflict: revision ${expectedRevision} changed before completion.`);
12109
12112
  }
12110
12113
  await event(id, "enrichment.completed", "system", "local-sourcebook-worker", { revision, coverage });
12114
+ await event(id, "publication.auto_published", "system", "local-sourcebook-worker", { revision });
12111
12115
  return getLocalSourcebookSubmission(id);
12112
12116
  }
12113
12117
  async function failLocalSourcebookAcquisition(id, message) {
@@ -12130,6 +12134,15 @@ async function listQueuedLocalSourcebookSubmissionIds(limit = 25) {
12130
12134
  async function publishLocalSourcebookSubmission(id) {
12131
12135
  const submission = await getLocalSourcebookSubmission(id);
12132
12136
  if (!submission) return null;
12137
+ const revision = await getDb().execute({
12138
+ sql: `SELECT actor_kind, actor_id FROM local_sourcebook_revisions WHERE submission_id = ? AND revision = ? LIMIT 1`,
12139
+ args: [id, submission.draftRevision]
12140
+ });
12141
+ const actorKind = revision.rows[0] ? String(revision.rows[0].actor_kind) : "";
12142
+ const actorId = revision.rows[0] ? String(revision.rows[0].actor_id) : "";
12143
+ if (actorKind !== "system" || actorId !== "local-sourcebook-worker") {
12144
+ throw new Error("Only a system-compiled Local Sourcebook revision can be published.");
12145
+ }
12133
12146
  await getDb().execute({ sql: `UPDATE local_sourcebook_submissions SET status = 'published', published_revision = draft_revision, updated_at = datetime('now') WHERE id = ?`, args: [id] });
12134
12147
  await event(id, "publication.published", "admin", "admin", { revision: submission.draftRevision });
12135
12148
  return getLocalSourcebookSubmission(id);
@@ -12146,6 +12159,14 @@ async function listPublicLocalSourcebook(category, state) {
12146
12159
  const result = await getDb().execute({ sql: `SELECT r.payload_json FROM local_sourcebook_submissions s JOIN local_sourcebook_revisions r ON r.submission_id = s.id AND r.revision = s.published_revision WHERE s.category = ? AND s.state = ? AND s.published_revision IS NOT NULL AND s.status NOT IN ('unpublished', 'rejected') ORDER BY s.business_name`, args: [category, state] });
12147
12160
  return result.rows.map((row) => parseJson2(row.payload_json));
12148
12161
  }
12162
+ async function listAllPublicLocalSourcebook(category) {
12163
+ await ensureLocalSourcebookSchema();
12164
+ const result = category ? await getDb().execute({
12165
+ sql: `SELECT r.payload_json FROM local_sourcebook_submissions s JOIN local_sourcebook_revisions r ON r.submission_id = s.id AND r.revision = s.published_revision WHERE s.category = ? AND s.published_revision IS NOT NULL AND s.status NOT IN ('unpublished', 'rejected') ORDER BY s.state, s.business_name`,
12166
+ args: [category]
12167
+ }) : await getDb().execute(`SELECT r.payload_json FROM local_sourcebook_submissions s JOIN local_sourcebook_revisions r ON r.submission_id = s.id AND r.revision = s.published_revision WHERE s.published_revision IS NOT NULL AND s.status NOT IN ('unpublished', 'rejected') ORDER BY s.category, s.state, s.business_name`);
12168
+ return result.rows.map((row) => parseJson2(row.payload_json));
12169
+ }
12149
12170
  async function getPublicLocalSourcebook(category, state, slug2) {
12150
12171
  await ensureLocalSourcebookSchema();
12151
12172
  const result = await getDb().execute({ sql: `SELECT r.payload_json FROM local_sourcebook_submissions s JOIN local_sourcebook_revisions r ON r.submission_id = s.id AND r.revision = s.published_revision WHERE s.category = ? AND s.state = ? AND s.slug = ? AND s.published_revision IS NOT NULL AND s.status NOT IN ('unpublished', 'rejected') LIMIT 1`, args: [category, state, slug2] });
@@ -12755,6 +12776,37 @@ var MapsExtractor = class {
12755
12776
 
12756
12777
  // src/api/local-sourcebook-compiler.ts
12757
12778
  import { createHash as createHash3 } from "crypto";
12779
+
12780
+ // src/api/local-sourcebook-public-urls.ts
12781
+ var DEFAULT_ROOT_DOMAIN = "localsourcebook.com";
12782
+ function rootDomain() {
12783
+ const configured = process.env.LOCAL_SOURCEBOOK_PUBLIC_ROOT_DOMAIN?.trim() || DEFAULT_ROOT_DOMAIN;
12784
+ return configured.replace(/^https?:\/\//i, "").replace(/^www\./i, "").replace(/\/.*$/, "").replace(/\.+$/, "").toLowerCase();
12785
+ }
12786
+ function localSourcebookPublicUrls(category, state, slug2) {
12787
+ const categoryUrl = `https://${category}.${rootDomain()}`;
12788
+ const statePath = `/${state.toLowerCase()}`;
12789
+ const profilePath = `${statePath}/${slug2}/`;
12790
+ const reviewsPath = `${statePath}/${slug2}/reviews`;
12791
+ return {
12792
+ categoryUrl,
12793
+ nationwideDirectoryUrl: `${categoryUrl}/directory`,
12794
+ stateUrl: `${categoryUrl}${statePath}`,
12795
+ profileUrl: `${categoryUrl}${profilePath}`,
12796
+ reviewsUrl: `${categoryUrl}${reviewsPath}`,
12797
+ profilePath,
12798
+ reviewsPath
12799
+ };
12800
+ }
12801
+ function localSourcebookPublicationLinks(submission) {
12802
+ return {
12803
+ ...localSourcebookPublicUrls(submission.category, submission.state, submission.slug),
12804
+ publicationStatus: submission.status,
12805
+ isLive: submission.status === "published" && submission.publishedRevision !== null
12806
+ };
12807
+ }
12808
+
12809
+ // src/api/local-sourcebook-compiler.ts
12758
12810
  var CATEGORY_LABELS = {
12759
12811
  home: "Home & property services",
12760
12812
  professional: "Professional services",
@@ -13107,6 +13159,7 @@ function compileLocalSourcebookListing(input) {
13107
13159
  evidenceIds: mapsEvidenceId ? [mapsEvidenceId] : []
13108
13160
  }
13109
13161
  ];
13162
+ const publicUrls = localSourcebookPublicUrls(input.submission.category, input.submission.state, input.submission.slug);
13110
13163
  const listing = {
13111
13164
  id: input.submission.id,
13112
13165
  publicationRevision: input.submission.draftRevision + 1,
@@ -13115,6 +13168,14 @@ function compileLocalSourcebookListing(input) {
13115
13168
  state: input.submission.state.toLowerCase(),
13116
13169
  stateName: STATE_NAMES[input.submission.state.toUpperCase()] ?? input.submission.state.toUpperCase(),
13117
13170
  slug: input.submission.slug,
13171
+ canonicalPaths: {
13172
+ profile: publicUrls.profilePath,
13173
+ reviews: publicUrls.reviewsPath
13174
+ },
13175
+ canonicalUrls: {
13176
+ profile: publicUrls.profileUrl,
13177
+ reviews: publicUrls.reviewsUrl
13178
+ },
13118
13179
  name: maps?.name || input.submission.businessName,
13119
13180
  summary,
13120
13181
  description,
@@ -13328,11 +13389,12 @@ async function runLocalSourcebookAcquisition(submission, provider = createDefaul
13328
13389
  await logRequestEvent({ userId: submission.ownerUserId, source: "local_sourcebook_maps", status: "done", query: submission.businessName, location: submission.state.toUpperCase(), resultCount: maps.reviews.length, result: { submissionId: submission.id, placeUrl: maps.placeUrl, cid: maps.cidDecimal ?? maps.cid, reviewsStatus: maps.reviewsStatus, reviewsRetained: maps.reviews.length, services: maps.services.length, areasServed: maps.areasServed.length } });
13329
13390
  } catch (error) {
13330
13391
  mapsError = errorMessage2(error);
13392
+ maps = null;
13331
13393
  await creditMcIdempotent(submission.ownerUserId, MC_COSTS.maps_place, LedgerOperation.REFUND, `Local Sourcebook failed exact-place acquisition: ${submission.id}`, `${billingCycle}:maps-failure-refund`);
13332
13394
  mapsRefunded = true;
13333
13395
  await logRequestEvent({ userId: submission.ownerUserId, source: "local_sourcebook_maps", status: "failed", query: submission.businessName, location: submission.state.toUpperCase(), error: mapsError });
13334
13396
  }
13335
- if (!site && !maps) throw new Error(`Website and exact-place acquisition both failed. Website: ${siteError || "not available"}. Maps: ${mapsError || "not available"}.`);
13397
+ if (!maps) throw new Error(`Exact-place acquisition is required before publication. Maps: ${mapsError || "not available"}. Website: ${siteError || (site ? "captured" : "not available")}.`);
13336
13398
  const compiled = compileLocalSourcebookListing({
13337
13399
  submission,
13338
13400
  site,
@@ -21255,23 +21317,6 @@ var DirectoryIdentitySchema = z25.object({
21255
21317
  tags: z25.array(z25.string().trim().min(1).max(60)).max(20).default([]),
21256
21318
  idempotencyKey: z25.string().trim().min(8).max(200)
21257
21319
  });
21258
- var DirectoryListingDraftSchema = z25.object({
21259
- id: z25.string().min(1),
21260
- category: z25.enum(LOCAL_SOURCEBOOK_CATEGORIES),
21261
- state: z25.string().trim().length(2),
21262
- slug: z25.string().trim().min(1),
21263
- name: z25.string().trim().min(2),
21264
- summary: z25.string().trim().min(20),
21265
- description: z25.string().trim().min(20),
21266
- website: z25.url(),
21267
- services: z25.array(z25.string()),
21268
- products: z25.array(z25.string()),
21269
- serviceAreas: z25.array(z25.string()),
21270
- media: z25.array(z25.record(z25.string(), z25.unknown())),
21271
- reviews: z25.array(z25.record(z25.string(), z25.unknown())),
21272
- evidence: z25.array(z25.record(z25.string(), z25.unknown())),
21273
- faq: z25.array(z25.object({ question: z25.string().min(3), answer: z25.string().min(12), evidenceIds: z25.array(z25.string()) }))
21274
- }).passthrough();
21275
21320
  function normalizeTag(value) {
21276
21321
  return value.toLowerCase().trim().replace(/&/g, " and ").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
21277
21322
  }
@@ -21352,18 +21397,22 @@ async function resolveLocalSourcebookTags(candidates) {
21352
21397
  function getLocalSourcebookContract(category) {
21353
21398
  return {
21354
21399
  name: "Local Sourcebook listing",
21355
- version: "local_sourcebook_listing_v1",
21400
+ version: "local_sourcebook_listing_v2",
21356
21401
  purpose: "An owner-scoped, evidence-backed local business record with a separate public profile and complete captured-review archive.",
21357
21402
  categories: LOCAL_SOURCEBOOK_CATEGORIES,
21358
21403
  selectedCategory: category ?? null,
21359
21404
  categoryTag: category ? CATEGORY_TAG[category] : null,
21360
- canonicalRoutes: { profile: "/{state}/{business-slug}/", reviews: "/{state}/{business-slug}/reviews" },
21405
+ canonicalRoutes: {
21406
+ nationwideDirectory: "https://{category}.localsourcebook.com/directory",
21407
+ profile: "https://{category}.localsourcebook.com/{state}/{business-slug}/",
21408
+ reviews: "https://{category}.localsourcebook.com/{state}/{business-slug}/reviews"
21409
+ },
21361
21410
  requiredCreateFields: ["category", "state", "businessName", "websiteUrl", "tags", "idempotencyKey"],
21362
- requiredDraftSections: ["identity", "services/products", "service areas/locations", "genuine media", "reviews and theme counts", "evidence", "answered FAQ", "collection receipts"],
21411
+ systemCompiledSections: ["identity", "services/products", "service areas/locations", "genuine media", "reviews and theme counts", "evidence", "answered FAQ", "collection receipts"],
21363
21412
  workflow: ["list-local-sourcebook-tags", "get-local-sourcebook-contract", "prepare-local-sourcebook-write", "validate-local-sourcebook-write", "local-sourcebook-capture", "local_sourcebook_submission_status"],
21364
- ownership: "Only the MCP Scraper account that captures a listing can read or revise its draft.",
21413
+ ownership: "Only the MCP Scraper account that captures a listing can read it or request a paid evidence refresh.",
21365
21414
  acquisition: { websitePagesMaximum: 100, reviewsMaximum: 500, exactMapsIdentityRequired: true, partialResultsRequireStoppingReasons: true },
21366
- publication: { automatic: false, adminReviewRequired: true, lastPublishedRevisionRemainsPublicDuringRefresh: true },
21415
+ publication: { automatic: true, evidenceCompiledOnly: true, ownerAuthoredClaimsAccepted: false, adminReviewRequired: false, lastPublishedRevisionRemainsPublicDuringRefresh: true },
21367
21416
  tagPolicy: { inspectVocabularyFirst: true, minimum: 1, maximum: 20, newTagsRequire: ["central=true", "reusable=true", "description"], newTagsStartAs: "pending" }
21368
21417
  };
21369
21418
  }
@@ -21377,16 +21426,18 @@ async function prepareLocalSourcebookWrite(input) {
21377
21426
  ...parsed.tags.filter((tag) => tag !== categoryTag).map((tag) => ({ tag }))
21378
21427
  ];
21379
21428
  const tagResolutions = await resolveLocalSourcebookTags(candidates);
21429
+ const publicUrls = localSourcebookPublicUrls(parsed.category, parsed.state, slug2);
21380
21430
  return {
21381
21431
  ok: true,
21382
- route: { category: parsed.category, state: parsed.state, slug: slug2, profile: `/${parsed.state}/${slug2}/`, reviews: `/${parsed.state}/${slug2}/reviews` },
21432
+ route: { category: parsed.category, state: parsed.state, slug: slug2, ...publicUrls },
21383
21433
  proposedWrite: { ...parsed, slug: slug2, tags: tagResolutions.filter((item) => item.action === "reuse").map((item) => item.tag) },
21384
21434
  contract: getLocalSourcebookContract(parsed.category),
21385
21435
  tagResolutions,
21386
21436
  instructions: [
21387
21437
  "Resolve every review candidate before capture; do not silently create a near-duplicate tag.",
21388
21438
  "Call validate-local-sourcebook-write with the proposed identity, resolved tags, and any required tag decisions.",
21389
- "Call local-sourcebook-capture only after validation returns valid=true. Capture creates a private draft and queues paid acquisition; it does not publish."
21439
+ "Call local-sourcebook-capture only after validation returns valid=true. Capture queues paid acquisition and automatically publishes the resulting system-compiled evidence revision.",
21440
+ "Owners cannot supply public listing claims directly. Request local_sourcebook_refresh when source-backed public facts need to be reacquired."
21390
21441
  ]
21391
21442
  };
21392
21443
  }
@@ -21394,17 +21445,9 @@ async function validateLocalSourcebookWrite(input) {
21394
21445
  const errors = [];
21395
21446
  const warnings = [];
21396
21447
  let normalizedIdentity2;
21397
- let normalizedListing;
21398
- if (input.submissionId) {
21399
- if (!Number.isInteger(input.baseRevision) || Number(input.baseRevision) < 1) errors.push("baseRevision is required for an existing draft edit.");
21400
- const parsedListing = DirectoryListingDraftSchema.safeParse(input.listing);
21401
- if (!parsedListing.success) errors.push(...parsedListing.error.issues.map((issue) => `listing.${issue.path.join(".")}: ${issue.message}`));
21402
- else normalizedListing = parsedListing.data;
21403
- } else {
21404
- const parsedIdentity = DirectoryIdentitySchema.safeParse(input.identity);
21405
- if (!parsedIdentity.success) errors.push(...parsedIdentity.error.issues.map((issue) => `identity.${issue.path.join(".")}: ${issue.message}`));
21406
- else normalizedIdentity2 = parsedIdentity.data;
21407
- }
21448
+ const parsedIdentity = DirectoryIdentitySchema.safeParse(input.identity);
21449
+ if (!parsedIdentity.success) errors.push(...parsedIdentity.error.issues.map((issue) => `identity.${issue.path.join(".")}: ${issue.message}`));
21450
+ else normalizedIdentity2 = parsedIdentity.data;
21408
21451
  const candidates = input.tagCandidates ?? (normalizedIdentity2?.tags.map((tag) => ({ tag })) ?? []);
21409
21452
  const resolutions = await resolveLocalSourcebookTags(candidates);
21410
21453
  const decisions = new Map((input.tagDecisions ?? []).map((decision) => [normalizeTag(decision.tag), decision]));
@@ -21423,12 +21466,12 @@ async function validateLocalSourcebookWrite(input) {
21423
21466
  }
21424
21467
  if (resolution.action === "omit") warnings.push(`Tag \u201C${resolution.candidate}\u201D will be omitted: ${resolution.reason}`);
21425
21468
  }
21426
- if (!input.submissionId && normalizedIdentity2) {
21469
+ if (normalizedIdentity2) {
21427
21470
  const categoryTag = CATEGORY_TAG[normalizedIdentity2.category];
21428
21471
  if (!normalizedTags.includes(categoryTag)) normalizedTags.unshift(categoryTag);
21429
21472
  }
21430
21473
  if (!normalizedTags.length) errors.push("At least one canonical directory tag is required.");
21431
- return { ok: true, valid: errors.length === 0, errors, warnings, normalizedIdentity: normalizedIdentity2, normalizedListing, normalizedTags: [...new Set(normalizedTags)], tagResolutions: resolutions };
21474
+ return { ok: true, valid: errors.length === 0, errors, warnings, normalizedIdentity: normalizedIdentity2, normalizedTags: [...new Set(normalizedTags)], tagResolutions: resolutions };
21432
21475
  }
21433
21476
  async function persistLocalSourcebookTags(submissionId, userId, tags, decisions = []) {
21434
21477
  await ensureLocalSourcebookTags();
@@ -21445,11 +21488,6 @@ async function persistLocalSourcebookTags(submissionId, userId, tags, decisions
21445
21488
  await db.execute({ sql: `INSERT OR IGNORE INTO local_sourcebook_submission_tags (submission_id, tag) VALUES (?, ?)`, args: [submissionId, tag] });
21446
21489
  }
21447
21490
  }
21448
- async function replaceLocalSourcebookTags(submissionId, userId, tags, decisions = []) {
21449
- await ensureLocalSourcebookTags();
21450
- await getDb().execute({ sql: `DELETE FROM local_sourcebook_submission_tags WHERE submission_id = ?`, args: [submissionId] });
21451
- await persistLocalSourcebookTags(submissionId, userId, tags, decisions);
21452
- }
21453
21491
 
21454
21492
  // src/api/local-sourcebook-routes.ts
21455
21493
  var slug = (value) => value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 100);
@@ -21461,13 +21499,9 @@ var SubmitSchema = z26.object({
21461
21499
  slug: z26.string().trim().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).max(100).optional(),
21462
21500
  idempotencyKey: z26.string().trim().min(8).max(200).optional()
21463
21501
  }).strict();
21464
- var DraftSchema = z26.object({ payload: z26.record(z26.string(), z26.unknown()) }).strict();
21465
21502
  var PrepareWriteSchema = DirectoryIdentitySchema.extend({ tagCandidates: z26.array(DirectoryTagCandidateSchema).max(20).optional() });
21466
21503
  var ValidateWriteSchema = z26.object({
21467
21504
  identity: DirectoryIdentitySchema.optional(),
21468
- listing: z26.record(z26.string(), z26.unknown()).optional(),
21469
- submissionId: z26.string().trim().min(1).optional(),
21470
- baseRevision: z26.number().int().min(1).optional(),
21471
21505
  tagCandidates: z26.array(DirectoryTagCandidateSchema).max(20).optional(),
21472
21506
  tagDecisions: z26.array(DirectoryTagDecisionSchema).max(20).optional()
21473
21507
  }).strict();
@@ -21477,7 +21511,7 @@ function acquisitionPlan(websiteUrl) {
21477
21511
  website: { startUrl: websiteUrl, mode: "broad_site_crawl", capture: ["homepage", "services", "products", "locations", "service_areas", "about", "team_staff", "contact", "policies"] },
21478
21512
  media: { requirement: "genuine_business_images_only", sources: ["business_website", "business_team_pages", "verified_business_profiles"] },
21479
21513
  reviews: { mode: "exhaust_accessible_continuation", requirement: "retain_each_review_and_source", disclosure: "report captured count, reported available count, and exact stopping reason" },
21480
- publication: { automatic: false, adminReviewRequired: true }
21514
+ publication: { automatic: true, evidenceCompiledOnly: true, adminReviewRequired: false }
21481
21515
  };
21482
21516
  }
21483
21517
  var localSourcebookApp = new Hono17();
@@ -21507,18 +21541,12 @@ localSourcebookApp.post("/capture", createApiKeyAuth(), async (c) => {
21507
21541
  const validation = await validateLocalSourcebookWrite(parsed.data);
21508
21542
  if (!validation.valid) return c.json({ error: "invalid_directory_write", ...validation }, 400);
21509
21543
  try {
21510
- if (parsed.data.submissionId) {
21511
- const submission2 = await getLocalSourcebookSubmission(parsed.data.submissionId, Number(user.id));
21512
- if (!submission2) return c.json({ error: "not_found" }, 404);
21513
- const updated = await reviseLocalSourcebookDraft(submission2.id, Number(user.id), validation.normalizedListing, parsed.data.baseRevision);
21514
- await replaceLocalSourcebookTags(submission2.id, Number(user.id), validation.normalizedTags, parsed.data.tagDecisions);
21515
- return c.json({ ok: true, valid: true, submission: updated, normalizedTags: validation.normalizedTags, captured: "draft_revision", acquisitionQueued: false });
21516
- }
21517
21544
  const identity = validation.normalizedIdentity;
21518
21545
  if (!parsed.data.idempotencyKey && !identity.idempotencyKey) {
21519
21546
  return c.json({ error: "idempotency_key_required", message: "A stable idempotencyKey is required for a new Local Sourcebook capture." }, 400);
21520
21547
  }
21521
21548
  const listingSlug = identity.slug ?? slug(identity.businessName);
21549
+ const publicUrls = localSourcebookPublicUrls(identity.category, identity.state, listingSlug);
21522
21550
  const submission = await createLocalSourcebookSubmission({
21523
21551
  ownerUserId: Number(user.id),
21524
21552
  category: identity.category,
@@ -21535,7 +21563,8 @@ localSourcebookApp.post("/capture", createApiKeyAuth(), async (c) => {
21535
21563
  name: identity.businessName,
21536
21564
  websiteUrl: identity.websiteUrl,
21537
21565
  tags: validation.normalizedTags,
21538
- canonicalPaths: { profile: `/${identity.state}/${listingSlug}`, reviews: `/${identity.state}/${listingSlug}/reviews` },
21566
+ canonicalPaths: { profile: publicUrls.profilePath, reviews: publicUrls.reviewsPath },
21567
+ canonicalUrls: { profile: publicUrls.profileUrl, reviews: publicUrls.reviewsUrl },
21539
21568
  verificationState: "submitted",
21540
21569
  acquisitionPlan: acquisitionPlan(identity.websiteUrl),
21541
21570
  evidence: [],
@@ -21545,7 +21574,7 @@ localSourcebookApp.post("/capture", createApiKeyAuth(), async (c) => {
21545
21574
  });
21546
21575
  await persistLocalSourcebookTags(submission.id, Number(user.id), validation.normalizedTags, parsed.data.tagDecisions);
21547
21576
  const dispatched = await dispatchLocalSourcebookAcquisition(submission.id).catch(() => false);
21548
- return c.json({ ok: true, valid: true, submission, normalizedTags: validation.normalizedTags, acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(identity.websiteUrl) }, 202);
21577
+ return c.json({ ok: true, valid: true, submission, publicUrls: localSourcebookPublicationLinks(submission), normalizedTags: validation.normalizedTags, acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(identity.websiteUrl) }, 202);
21549
21578
  } catch (error) {
21550
21579
  const message = error instanceof Error ? error.message : String(error);
21551
21580
  const conflict = message.includes("revision conflict") ? "revision_conflict" : message.includes("idempotency key conflicts") ? "idempotency_conflict" : null;
@@ -21559,6 +21588,7 @@ localSourcebookApp.post("/submissions", createApiKeyAuth(), async (c) => {
21559
21588
  if (!parsed.success) return c.json({ error: "invalid_submission", issues: parsed.error.issues }, 400);
21560
21589
  const input = parsed.data;
21561
21590
  const listingSlug = input.slug ?? slug(input.businessName);
21591
+ const publicUrls = localSourcebookPublicUrls(input.category, input.state, listingSlug);
21562
21592
  try {
21563
21593
  const submission = await createLocalSourcebookSubmission({
21564
21594
  ownerUserId: Number(user.id),
@@ -21571,7 +21601,8 @@ localSourcebookApp.post("/submissions", createApiKeyAuth(), async (c) => {
21571
21601
  slug: listingSlug,
21572
21602
  name: input.businessName,
21573
21603
  websiteUrl: input.websiteUrl,
21574
- canonicalPaths: { profile: `/${input.state}/${listingSlug}`, reviews: `/${input.state}/${listingSlug}/reviews` },
21604
+ canonicalPaths: { profile: publicUrls.profilePath, reviews: publicUrls.reviewsPath },
21605
+ canonicalUrls: { profile: publicUrls.profileUrl, reviews: publicUrls.reviewsUrl },
21575
21606
  verificationState: "submitted",
21576
21607
  acquisitionPlan: acquisitionPlan(input.websiteUrl),
21577
21608
  evidence: [],
@@ -21580,7 +21611,7 @@ localSourcebookApp.post("/submissions", createApiKeyAuth(), async (c) => {
21580
21611
  }
21581
21612
  });
21582
21613
  const dispatched = await dispatchLocalSourcebookAcquisition(submission.id).catch(() => false);
21583
- return c.json({ ok: true, submission, acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(input.websiteUrl) }, 202);
21614
+ return c.json({ ok: true, submission, publicUrls: localSourcebookPublicationLinks(submission), acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(input.websiteUrl) }, 202);
21584
21615
  } catch (error) {
21585
21616
  return c.json({ error: "submission_conflict", message: error instanceof Error ? error.message : String(error) }, 409);
21586
21617
  }
@@ -21589,26 +21620,27 @@ localSourcebookApp.get("/submissions/:id", createApiKeyAuth(), async (c) => {
21589
21620
  const userId = Number(c.get("user").id);
21590
21621
  const submission = await getLocalSourcebookSubmission(c.req.param("id"), userId);
21591
21622
  if (!submission) return c.json({ error: "not_found" }, 404);
21592
- return c.json({ ok: true, submission, draft: await getLocalSourcebookDraft(submission.id, userId) });
21593
- });
21594
- localSourcebookApp.on(["PATCH", "POST"], "/submissions/:id/draft", createApiKeyAuth(), async (c) => {
21595
- const user = c.get("user");
21596
- if (!user.subscription_tier) return c.json({ error: "subscription_required" }, 402);
21597
- const parsed = DraftSchema.safeParse(await c.req.json().catch(() => null));
21598
- if (!parsed.success) return c.json({ error: "invalid_draft", issues: parsed.error.issues }, 400);
21599
- const submission = await reviseLocalSourcebookDraft(c.req.param("id"), Number(user.id), parsed.data.payload);
21600
- if (!submission) return c.json({ error: "not_found" }, 404);
21601
- return c.json({ ok: true, submission });
21623
+ return c.json({ ok: true, submission, publicUrls: localSourcebookPublicationLinks(submission), draft: await getLocalSourcebookDraft(submission.id, userId) });
21602
21624
  });
21625
+ localSourcebookApp.on(["PATCH", "POST"], "/submissions/:id/draft", createApiKeyAuth(), (c) => c.json({
21626
+ error: "evidence_refresh_required",
21627
+ message: "Owner-authored listing payloads are not accepted. Request a refresh so MCP Scraper can reacquire and compile the public evidence."
21628
+ }, 409));
21603
21629
  localSourcebookApp.post("/submissions/:id/refresh", createApiKeyAuth(), async (c) => {
21604
21630
  const user = c.get("user");
21605
21631
  if (!user.subscription_tier) return c.json({ error: "subscription_required" }, 402);
21606
- const submission = await queueLocalSourcebookRefresh(c.req.param("id"), Number(user.id));
21607
- if (!submission) return c.json({ error: "not_found" }, 404);
21608
- const dispatched = await dispatchLocalSourcebookAcquisition(submission.id).catch(() => false);
21609
- return c.json({ ok: true, submission, acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(submission.websiteUrl) }, 202);
21632
+ try {
21633
+ const submission = await queueLocalSourcebookRefresh(c.req.param("id"), Number(user.id));
21634
+ if (!submission) return c.json({ error: "not_found" }, 404);
21635
+ const dispatched = await dispatchLocalSourcebookAcquisition(submission.id).catch(() => false);
21636
+ return c.json({ ok: true, submission, publicUrls: localSourcebookPublicationLinks(submission), acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(submission.websiteUrl) }, 202);
21637
+ } catch (error) {
21638
+ return c.json({ error: "moderated_listing", message: error instanceof Error ? error.message : String(error) }, 409);
21639
+ }
21610
21640
  });
21611
21641
  var publicLocalSourcebookApp = new Hono17();
21642
+ publicLocalSourcebookApp.get("/", async (c) => c.json({ ok: true, listings: await listAllPublicLocalSourcebook() }));
21643
+ publicLocalSourcebookApp.get("/:category", async (c) => c.json({ ok: true, listings: await listAllPublicLocalSourcebook(c.req.param("category")) }));
21612
21644
  publicLocalSourcebookApp.get("/:category/:state", async (c) => c.json({ ok: true, listings: await listPublicLocalSourcebook(c.req.param("category"), c.req.param("state").toLowerCase()) }));
21613
21645
  publicLocalSourcebookApp.get("/:category/:state/:slug", async (c) => {
21614
21646
  const listing = await getPublicLocalSourcebook(c.req.param("category"), c.req.param("state").toLowerCase(), c.req.param("slug"));
@@ -40986,4 +41018,4 @@ app.get("/blog/:slug/", (c) => {
40986
41018
  export {
40987
41019
  app
40988
41020
  };
40989
- //# sourceMappingURL=server-D2Z7BF7S.js.map
41021
+ //# sourceMappingURL=server-JJLFY46K.js.map