open-local-audit 0.68.0 → 0.69.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.
package/dist/start.js ADDED
@@ -0,0 +1,234 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { createInterface } from "node:readline/promises";
4
+ const categories = [
5
+ { query: "dental", profile: "dental" },
6
+ { query: "restaurant", profile: "restaurant" },
7
+ { query: "beauty", profile: "beauty" },
8
+ { query: "hotel", profile: "hotel" },
9
+ { query: "gym", profile: "gym" }
10
+ ];
11
+ const overtureCategoryPattern = /^[a-z][a-z0-9_]{0,80}$/i;
12
+ function defaultPromptIo() {
13
+ const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
14
+ const reader = isTTY ? createInterface({ input: process.stdin, output: process.stdout, terminal: true }) : undefined;
15
+ let closed = false;
16
+ const close = () => {
17
+ if (!closed) {
18
+ closed = true;
19
+ reader?.close();
20
+ }
21
+ };
22
+ reader?.on("SIGINT", close);
23
+ return {
24
+ isTTY,
25
+ question: async (prompt) => {
26
+ if (!reader || closed)
27
+ return null;
28
+ return new Promise((answer) => {
29
+ const finish = (value) => {
30
+ reader.off("close", onClose);
31
+ answer(value);
32
+ };
33
+ const onClose = () => finish(null);
34
+ reader.once("close", onClose);
35
+ reader.question(prompt).then((value) => finish(value), () => finish(null));
36
+ });
37
+ },
38
+ write: (message) => process.stdout.write(message),
39
+ close,
40
+ cwd: () => process.cwd(),
41
+ exists: async (path) => {
42
+ try {
43
+ await lstat(path);
44
+ return true;
45
+ }
46
+ catch (error) {
47
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
48
+ return false;
49
+ throw error;
50
+ }
51
+ },
52
+ now: () => new Date()
53
+ };
54
+ }
55
+ async function answer(io, prompt) {
56
+ try {
57
+ return await io.question(prompt);
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function writeLine(io, message) {
64
+ io.write(`${message}\n`);
65
+ }
66
+ function slug(value) {
67
+ const normalized = value
68
+ .normalize("NFKD")
69
+ .replace(/\p{M}/gu, "")
70
+ .toLowerCase()
71
+ .replace(/[^a-z0-9]+/g, "-")
72
+ .replace(/^-+|-+$/g, "");
73
+ return normalized || "search";
74
+ }
75
+ function timestamp(value) {
76
+ const pad = (part) => String(part).padStart(2, "0");
77
+ return `${value.getUTCFullYear()}${pad(value.getUTCMonth() + 1)}${pad(value.getUTCDate())}-${pad(value.getUTCHours())}${pad(value.getUTCMinutes())}${pad(value.getUTCSeconds())}`;
78
+ }
79
+ async function promptCountry(io) {
80
+ for (;;) {
81
+ const value = await answer(io, "Country ISO code (two letters): ");
82
+ if (value === null)
83
+ return null;
84
+ const country = value.trim().toUpperCase();
85
+ if (/^[A-Z]{2}$/.test(country))
86
+ return country;
87
+ writeLine(io, "Country must use a two-letter ISO code, for example TR.");
88
+ }
89
+ }
90
+ async function promptCity(io) {
91
+ for (;;) {
92
+ const value = await answer(io, "City: ");
93
+ if (value === null)
94
+ return null;
95
+ const city = value.trim();
96
+ if (city)
97
+ return city;
98
+ writeLine(io, "City is required.");
99
+ }
100
+ }
101
+ async function promptCategory(io) {
102
+ writeLine(io, "Category: 1) dental 2) restaurant 3) beauty 4) hotel 5) gym 6) custom Overture category");
103
+ for (;;) {
104
+ const selected = await answer(io, "Choose a category [1-6]: ");
105
+ if (selected === null)
106
+ return null;
107
+ if (!/^[1-6]$/.test(selected.trim())) {
108
+ writeLine(io, "Choose a category number from 1 to 6.");
109
+ continue;
110
+ }
111
+ const index = Number(selected.trim()) - 1;
112
+ if (index < categories.length)
113
+ return categories[index];
114
+ for (;;) {
115
+ const custom = await answer(io, "Custom Overture category: ");
116
+ if (custom === null)
117
+ return null;
118
+ const query = custom.trim();
119
+ if (overtureCategoryPattern.test(query))
120
+ return { query, profile: "generic" };
121
+ writeLine(io, "Use an Overture category identifier beginning with a letter and containing only letters, digits, or underscores.");
122
+ }
123
+ }
124
+ }
125
+ async function promptWholeNumber(io, prompt, defaultValue, minimum, maximum, invalidMessage) {
126
+ for (;;) {
127
+ const input = await answer(io, `${prompt} [${defaultValue}]: `);
128
+ if (input === null)
129
+ return null;
130
+ const value = input.trim();
131
+ if (!value)
132
+ return defaultValue;
133
+ if (/^(?:0|[1-9]\d*)$/.test(value)) {
134
+ const parsed = Number(value);
135
+ if (Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum)
136
+ return parsed;
137
+ }
138
+ writeLine(io, invalidMessage);
139
+ }
140
+ }
141
+ async function uniqueDefaultDirectory(io, country, city, category) {
142
+ const base = resolve(io.cwd(), "reports", `${country.toLowerCase()}-${slug(city)}-${slug(category)}-${timestamp(io.now())}`);
143
+ let candidate = base;
144
+ let suffix = 2;
145
+ while (await io.exists(candidate)) {
146
+ candidate = `${base}-${suffix}`;
147
+ suffix += 1;
148
+ }
149
+ return candidate;
150
+ }
151
+ async function promptOutputDirectory(io, defaultDirectory) {
152
+ for (;;) {
153
+ const input = await answer(io, `Output directory [${defaultDirectory}]: `);
154
+ if (input === null)
155
+ return null;
156
+ const directory = input.trim() ? resolve(io.cwd(), input.trim()) : defaultDirectory;
157
+ if (!await io.exists(directory))
158
+ return directory;
159
+ writeLine(io, "Output path already exists. Choose a new directory so existing results are not overwritten.");
160
+ }
161
+ }
162
+ function showSummary(io, options) {
163
+ writeLine(io, "");
164
+ writeLine(io, "Discovery summary");
165
+ writeLine(io, ` Location: ${options.city}, ${options.country}`);
166
+ writeLine(io, ` Category: ${options.query}`);
167
+ writeLine(io, ` Candidate limit: ${options.limit}`);
168
+ writeLine(io, ` Website audit cap: ${options.maxAudits}`);
169
+ writeLine(io, ` Output: ${options.outDir}`);
170
+ writeLine(io, " Free API-keyless Overture discovery; results are stored in local files.");
171
+ }
172
+ export async function collectStartOptions(io = defaultPromptIo()) {
173
+ try {
174
+ if (!io.isTTY) {
175
+ throw new Error("Interactive start requires a terminal. Use discover dental --city Istanbul --country TR --dry-run --export-csv leads.csv instead.");
176
+ }
177
+ const country = await promptCountry(io);
178
+ if (country === null)
179
+ return null;
180
+ const city = await promptCity(io);
181
+ if (city === null)
182
+ return null;
183
+ const category = await promptCategory(io);
184
+ if (category === null)
185
+ return null;
186
+ const limit = await promptWholeNumber(io, "Candidate count", 10, 1, 100, "Candidate count must be a whole number from 1 to 100.");
187
+ if (limit === null)
188
+ return null;
189
+ const maxAudits = await promptWholeNumber(io, "Website audit cap", Math.min(3, limit), 0, limit, `Website audit cap must be a whole number from 0 to ${limit}.`);
190
+ if (maxAudits === null)
191
+ return null;
192
+ const defaultDirectory = await uniqueDefaultDirectory(io, country, city, category.query);
193
+ let outDir = await promptOutputDirectory(io, defaultDirectory);
194
+ if (outDir === null)
195
+ return null;
196
+ for (;;) {
197
+ showSummary(io, { city, country, query: category.query, limit, maxAudits, outDir });
198
+ const confirmation = await answer(io, "Start? [Y/n]: ");
199
+ if (confirmation === null)
200
+ return null;
201
+ const normalized = confirmation.trim().toLowerCase();
202
+ if (!normalized || normalized === "y" || normalized === "yes") {
203
+ if (!await io.exists(outDir)) {
204
+ return {
205
+ provider: "overture",
206
+ query: category.query,
207
+ city,
208
+ country,
209
+ profile: category.profile,
210
+ limit,
211
+ maxAudits,
212
+ concurrency: 3,
213
+ dryRun: maxAudits === 0,
214
+ outDir,
215
+ exportCsv: join(outDir, "leads.csv"),
216
+ summaryJson: join(outDir, "discovery-summary.json")
217
+ };
218
+ }
219
+ writeLine(io, "Output path already exists. Choose a new directory so existing results are not overwritten.");
220
+ outDir = await promptOutputDirectory(io, defaultDirectory);
221
+ if (outDir === null)
222
+ return null;
223
+ continue;
224
+ }
225
+ if (normalized === "n" || normalized === "no")
226
+ return null;
227
+ writeLine(io, "Please answer Y or n.");
228
+ }
229
+ }
230
+ finally {
231
+ io.close();
232
+ }
233
+ }
234
+ //# sourceMappingURL=start.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start.js","sourceRoot":"","sources":["../src/start.ts"],"names":[],"mappings":"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"}
@@ -158,7 +158,6 @@ declare const workflowConfigSchema: z.ZodObject<{
158
158
  packageReports: z.ZodDefault<z.ZodBoolean>;
159
159
  }, "strict", z.ZodTypeAny, {
160
160
  outDir: string;
161
- version: 1;
162
161
  discovery: {
163
162
  profile: "generic" | "dental" | "beauty" | "restaurant" | "contractor" | "lawyer" | "clinic" | "gym" | "hotel" | "auto-service";
164
163
  input: string;
@@ -182,6 +181,7 @@ declare const workflowConfigSchema: z.ZodObject<{
182
181
  release?: string | undefined;
183
182
  maxAudits?: number | undefined;
184
183
  };
184
+ version: 1;
185
185
  shortlist: {
186
186
  sort: "opportunity-desc" | "score-desc" | "company-asc" | "last-reviewed-asc" | "contact-confidence-desc" | "priority-desc" | "source-asc";
187
187
  top: number;
@@ -215,7 +215,6 @@ declare const workflowConfigSchema: z.ZodObject<{
215
215
  } | undefined;
216
216
  }, {
217
217
  outDir: string;
218
- version: 1;
219
218
  discovery: {
220
219
  input: string;
221
220
  provider: "manual-csv";
@@ -239,6 +238,7 @@ declare const workflowConfigSchema: z.ZodObject<{
239
238
  limit?: number | undefined;
240
239
  maxAudits?: number | undefined;
241
240
  };
241
+ version: 1;
242
242
  shortlist: {
243
243
  profile?: string | undefined;
244
244
  sort?: "opportunity-desc" | "score-desc" | "company-asc" | "last-reviewed-asc" | "contact-confidence-desc" | "priority-desc" | "source-asc" | undefined;
@@ -4,6 +4,14 @@ The `overture` provider finds business candidates without an API key, payment ac
4
4
 
5
5
  ## Search by category and location
6
6
 
7
+ For a guided first search from a built source checkout:
8
+
9
+ ```bash
10
+ node dist/cli.js start
11
+ ```
12
+
13
+ The terminal asks for a country code, city, category, candidate count, website-audit cap, and output directory. It previews the search before confirmation. Enter `n` at confirmation or press Ctrl+C to cancel. No discovery requests or output files are created before confirmation. Existing output directories are refused to protect earlier results. Piped/non-interactive use exits with an error and an explicit `discover` example; use that command in scripts.
14
+
7
15
  From a source checkout, install dependencies and build:
8
16
 
9
17
  Use Node.js 22.19 or newer.
@@ -26,6 +34,26 @@ node dist/cli.js discover beauty --bbox "28.85,40.95,29.10,41.10" --profile beau
26
34
 
27
35
  Friendly categories include `dental`, `restaurant`, `beauty`, `hotel`, and `gym`; official Overture taxonomy identifiers also work. Matching includes descendants. `--profile` selects existing audit rules separately from the discovery category. No matches do not prove no businesses exist in the area.
28
36
 
37
+ ## Repeated-search cache
38
+
39
+ `discover` with Overture and `start` enable the local business-result cache by default. An entry is reusable only for the same normalized category, numeric bounding box, candidate limit, and Overture release, and for at most seven days. A different audit profile reuses source data with the newly selected profile. Website audits are never taken from this cache; they run again up to the requested audit cap.
40
+
41
+ Without `--release`, the current Overture release is checked before looking in the cache. This still needs a small network request; an old cached release is not silently used if the release lookup fails. A pinned `--release` can reuse its valid cache without querying STAC or the remote Places files. Cached results keep their original retrieval time; neither that time nor the release date means the business details were independently verified.
42
+
43
+ - `--refresh-cache` bypasses the saved result and replaces that search's entry after a successful lookup.
44
+ - `--no-cache` disables business-result cache reads and writes.
45
+ - `--cache-dir <path>` selects another directory; relative paths resolve from the current working directory. It cannot be combined with `--no-cache`.
46
+
47
+ ```bash
48
+ node dist/cli.js discover dental --city Istanbul --country TR --profile dental --limit 10 --dry-run --cache-dir reports/search-cache --export-csv reports/leads.csv
49
+ ```
50
+
51
+ The CLI reports `hit`, `miss`, `refresh`, `disabled`, or `unavailable`, alongside the source release and original fetch time when available. Invalid, expired, oversized, or unsafe entries do not become cache hits. Cache storage failures fall back to a fresh search with a diagnostic; an actual source-search failure remains an error.
52
+
53
+ Default storage is `%LOCALAPPDATA%/open-local-audit/cache/discovery` on Windows, `~/Library/Caches/open-local-audit/discovery` on macOS, and `$XDG_CACHE_HOME/open-local-audit/discovery` (or `~/.cache/open-local-audit/discovery`) on Linux. It contains public source business details, not API keys or website audit reports. Each entry is limited to 2 MiB; expired entries are replaced when searched again, rather than removed by a background job.
54
+
55
+ The existing GeoNames city-file cache remains separate under the output directory. Programmatic discovery opts into business caching with `cacheDir`; the version-1 `workflow` command keeps its existing uncached execution and output contract.
56
+
29
57
  ## Data and limits
30
58
 
31
59
  - Overture cloud GeoParquet is queried using local DuckDB. First use installs its `httpfs` extension and requires network access to the extension repository, Overture STAC and S3. A full-world dataset is not intentionally downloaded.
@@ -78,6 +106,14 @@ npm run benchmark:discovery -- --out-dir reports/free-discovery-benchmark
78
106
 
79
107
  The default 24-search matrix uses Istanbul/Ankara (Türkiye), New York/Los Angeles (USA), Berlin/Munich (Germany), and London/Manchester (UK), each for dental, restaurant, and beauty. These are major-city samples, not an official popularity ranking. Every search uses a documented city-centre box, up to ten candidates, up to three website audits, and concurrency three. The first returned release is pinned for later searches.
80
108
 
109
+ The benchmark leaves business caching disabled unless `--cache-dir` is explicitly supplied, preserving fresh-search measurements. To compare the same search before and after caching, use a new cache directory and inspect the recorded `miss`/`hit` states:
110
+
111
+ ```bash
112
+ npm run benchmark:discovery -- --out-dir reports/cache-benchmark --cache-dir reports/cache-benchmark/first-cache --case-filter tr-istanbul-dental --repeats 2 --max-audits 0 --release 2026-09-23.1
113
+ ```
114
+
115
+ `--max-audits 0` isolates discovery from website work. A previously populated cache may produce a hit on both runs; do not label the first run cold unless its recorded state is `miss`. Per-case JSON retains cache state and original fetch time; the CSV and Markdown identify cached runs. A single pair is an indicative measurement, not a speed guarantee or a load test.
116
+
81
117
  Case JSON and prospect/report outputs are saved as each case completes, followed by summary JSON, CSV, and Markdown. Failure times remain in the evidence. Discovery and enrichment/report times are separate. Contact completeness among returned rows is not market coverage, recall, or contact correctness. Percentiles across different scenarios are descriptive, not load-test results or an SLA. One pass does not establish stable latency. Use `--repeats 2` to intentionally measure another pass, or `--case-filter Istanbul` to limit the matrix.
82
118
 
83
119
  ## Sources and attribution
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-local-audit",
3
- "version": "0.68.0",
3
+ "version": "0.69.0",
4
4
  "description": "Open-source website and local presence auditor for small businesses.",
5
5
  "license": "MIT",
6
6
  "type": "module",