@gscdump/engine 1.4.7 → 1.4.10

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.
@@ -1,6 +1,6 @@
1
1
  import { DataSource, StorageEngine } from "../storage.mjs";
2
- import { SearchType } from "gscdump/query";
3
2
  import { Row, TableName, TenantCtx } from "@gscdump/contracts";
3
+ import { SearchType } from "gscdump/query";
4
4
  interface NodeHarnessOptions {
5
5
  dataDir: string;
6
6
  /** Tenant user id. Defaults to `'local'` for single-user CLI installs. */
@@ -4,6 +4,7 @@ import { SitemapGenerationKey, emptyTypesKey, hashSortedUrlList, hashUrl, hashUr
4
4
  import { QueryDimDeps, QueryDimMeta, QueryDimRecord, QueryDimStore, buildQueryDimRecords, createQueryDimStore } from "./query-dim.mjs";
5
5
  import { SITEMAP_PROJECTION_GRACE_MS, SitemapProjectionFeed, SitemapProjectionFiles, SitemapProjectionManifest, decodeSitemapProjectionManifest, emptySitemapProjectionManifest, parseSitemapProjectionManifest, selectSitemapProjectionFiles, withSitemapProjectionFeed } from "./sitemap-projection.mjs";
6
6
  import { TenantCtx } from "@gscdump/contracts";
7
+ import { CanonicalDifferenceKind } from "gscdump";
7
8
  /**
8
9
  * GSC URL inspection result fields we persist. Mirrors the
9
10
  * `searchconsole_v1.Schema$UrlInspectionResult` shape but as plain JSON
@@ -106,6 +107,8 @@ interface InspectionParquetRow {
106
107
  * newest-by-`inspectedAt` event.
107
108
  */
108
109
  interface InspectionEventRow extends InspectionParquetRow {
110
+ /** Declared-vs-selected canonical classification, computed at inspection ingest. */
111
+ canonicalMismatchKind: CanonicalDifferenceKind;
109
112
  crawlingUserAgent: string | null;
110
113
  /** JSON-encoded `RichResultsItem[]`. */
111
114
  richResultsItems: string | null;
@@ -212,6 +215,15 @@ interface InspectionStore {
212
215
  eventFilesDeleted: number;
213
216
  transitionsWritten: number;
214
217
  }>;
218
+ /**
219
+ * Rewrite a legacy latest-only base with the canonical kind derived from the
220
+ * canonical pair it already retains. Outstanding events must be compacted first.
221
+ */
222
+ backfillCanonicalMismatchKinds: (ctx: TenantCtx) => Promise<{
223
+ baseRowCount: number;
224
+ rowsBackfilled: number;
225
+ rewritten: boolean;
226
+ }>;
215
227
  /**
216
228
  * DuckDB-resolvable URI for the materialised parquet sidecar, or
217
229
  * `undefined` if the underlying `DataSource` has no native URI shape
package/dist/entities.mjs CHANGED
@@ -4,6 +4,8 @@ import { readOptional } from "./adapters/read-optional.mjs";
4
4
  import { SITEMAP_PROJECTION_GRACE_MS, decodeSitemapProjectionManifest, emptySitemapProjectionManifest, parseSitemapProjectionManifest, selectSitemapProjectionFiles, withSitemapProjectionFeed } from "./sitemap-projection.mjs";
5
5
  import { buildQueryDimRecords, createQueryDimStore } from "./query-dim.mjs";
6
6
  import { encodeJsonBigintSafe } from "@gscdump/lakehouse/bigint";
7
+ import { GSCDUMP_INDEXING_TRANSITION_FIELDS } from "@gscdump/contracts";
8
+ import { classifyCanonicalDifference } from "gscdump";
7
9
  const YEAR_MONTH_RE = /^(\d{4})-(\d{2})-/;
8
10
  const ENTITY_IO_CONCURRENCY = 8;
9
11
  async function mapEntityIo(items, fn) {
@@ -106,6 +108,11 @@ const INSPECTION_PARQUET_COLUMNS = [
106
108
  ];
107
109
  const INSPECTION_EVENT_COLUMNS = [
108
110
  ...INSPECTION_PARQUET_COLUMNS,
111
+ {
112
+ name: "canonicalMismatchKind",
113
+ type: "VARCHAR",
114
+ nullable: false
115
+ },
109
116
  {
110
117
  name: "crawlingUserAgent",
111
118
  type: "VARCHAR",
@@ -157,14 +164,19 @@ const INSPECTION_EVENT_COLUMNS = [
157
164
  nullable: true
158
165
  }
159
166
  ];
160
- const TRANSITION_STATE_FIELDS = [
161
- "indexStatus",
162
- "coverageState",
163
- "robotsTxtState",
164
- "indexingState",
165
- "pageFetchState",
166
- "googleCanonical"
167
- ];
167
+ const CANONICAL_DIFFERENCE_KINDS = /* @__PURE__ */ new Set([
168
+ "none",
169
+ "formatting",
170
+ "path",
171
+ "cross_domain"
172
+ ]);
173
+ function populateCanonicalMismatchKind(row) {
174
+ const current = row.canonicalMismatchKind;
175
+ const expected = classifyCanonicalDifference(typeof row.userCanonical === "string" ? row.userCanonical : null, typeof row.googleCanonical === "string" ? row.googleCanonical : null);
176
+ const missingOrInvalid = typeof current !== "string" || !CANONICAL_DIFFERENCE_KINDS.has(current);
177
+ row.canonicalMismatchKind = expected;
178
+ return missingOrInvalid || current !== expected;
179
+ }
168
180
  const INSPECTION_TRANSITION_COLUMNS = [
169
181
  {
170
182
  name: "urlHash",
@@ -186,7 +198,7 @@ const INSPECTION_TRANSITION_COLUMNS = [
186
198
  type: "VARCHAR",
187
199
  nullable: false
188
200
  },
189
- ...TRANSITION_STATE_FIELDS.flatMap((field) => {
201
+ ...GSCDUMP_INDEXING_TRANSITION_FIELDS.flatMap((field) => {
190
202
  const capped = field[0].toUpperCase() + field.slice(1);
191
203
  return [{
192
204
  name: `from${capped}`,
@@ -200,7 +212,7 @@ const INSPECTION_TRANSITION_COLUMNS = [
200
212
  })
201
213
  ];
202
214
  function isStateTransition(before, after) {
203
- return TRANSITION_STATE_FIELDS.some((field) => (before[field] ?? null) !== (after[field] ?? null));
215
+ return GSCDUMP_INDEXING_TRANSITION_FIELDS.some((field) => (before[field] ?? null) !== (after[field] ?? null));
204
216
  }
205
217
  function buildTransitionRow(before, after) {
206
218
  const row = {
@@ -209,7 +221,7 @@ function buildTransitionRow(before, after) {
209
221
  changedAfter: String(before.inspectedAt ?? ""),
210
222
  changedBefore: String(after.inspectedAt ?? "")
211
223
  };
212
- for (const field of TRANSITION_STATE_FIELDS) {
224
+ for (const field of GSCDUMP_INDEXING_TRANSITION_FIELDS) {
213
225
  const capped = field[0].toUpperCase() + field.slice(1);
214
226
  row[`from${capped}`] = before[field] ?? null;
215
227
  row[`to${capped}`] = after[field] ?? null;
@@ -396,6 +408,7 @@ function createInspectionStore(opts) {
396
408
  for (const [h, row] of latest) {
397
409
  const fc = earliestChecked.get(h);
398
410
  if (fc !== void 0) row.firstCheckedAt = fc;
411
+ populateCanonicalMismatchKind(row);
399
412
  merged.push(row);
400
413
  }
401
414
  const bytes = encodeRowsToParquetFlex(merged, {
@@ -412,6 +425,32 @@ function createInspectionStore(opts) {
412
425
  transitionsWritten
413
426
  };
414
427
  },
428
+ async backfillCanonicalMismatchKinds(ctx) {
429
+ const baseKey = inspectionBaseKey(ctx);
430
+ const baseBytes = await readOptional(ds, baseKey);
431
+ if (!baseBytes) return {
432
+ baseRowCount: 0,
433
+ rowsBackfilled: 0,
434
+ rewritten: false
435
+ };
436
+ const rows = await decodeParquetToRows(baseBytes);
437
+ let rowsBackfilled = 0;
438
+ for (const row of rows) if (populateCanonicalMismatchKind(row)) rowsBackfilled++;
439
+ if (rowsBackfilled === 0) return {
440
+ baseRowCount: rows.length,
441
+ rowsBackfilled: 0,
442
+ rewritten: false
443
+ };
444
+ await ds.write(baseKey, encodeRowsToParquetFlex(rows, {
445
+ columns: INSPECTION_EVENT_COLUMNS,
446
+ sortKey: ["urlHash"]
447
+ }));
448
+ return {
449
+ baseRowCount: rows.length,
450
+ rowsBackfilled,
451
+ rewritten: true
452
+ };
453
+ },
415
454
  parquetUri(ctx) {
416
455
  return ds.uri?.(inspectionParquetKey(ctx));
417
456
  }
package/dist/layout.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CompactionTier, ManifestEntry } from "./storage.mjs";
2
- import { SearchType } from "gscdump/query";
3
2
  import { TableName, TenantCtx } from "@gscdump/contracts";
3
+ import { SearchType } from "gscdump/query";
4
4
  declare function dayPartition(date: string): string;
5
5
  /**
6
6
  * Hourly partition keyed by the PT calendar day (`YYYY-MM-DD`). One parquet
@@ -1,7 +1,7 @@
1
1
  import { SearchType as SearchType$1, TableName as TableName$1 } from "../storage.mjs";
2
2
  import { ComparisonFilter } from "./types.mjs";
3
- import { BuilderState } from "gscdump/query";
4
3
  import { Grain } from "@gscdump/contracts";
4
+ import { BuilderState } from "gscdump/query";
5
5
  interface RunQueryCtx {
6
6
  userId: string;
7
7
  siteId: string;
@@ -2,8 +2,8 @@ import { DataSource, FileSetRef, Row as Row$1, TableName as TableName$1 } from "
2
2
  import { ColumnDef } from "./schema.mjs";
3
3
  import { EngineError } from "./errors.mjs";
4
4
  import "./contracts.mjs";
5
- import { SearchType } from "gscdump/query";
6
5
  import { TenantCtx } from "@gscdump/contracts";
6
+ import { SearchType } from "gscdump/query";
7
7
  interface RollupCtx extends TenantCtx {
8
8
  /** When the rollup was built. Stamped into payload + filename. */
9
9
  builtAt: number;
package/dist/rollups.mjs CHANGED
@@ -978,7 +978,7 @@ const indexingHealthRollup = {
978
978
  SUM(CASE WHEN CAST(pageFetchState AS VARCHAR) = 'NOT_FOUND' THEN 1 ELSE 0 END)::BIGINT AS not_found,
979
979
  SUM(CASE WHEN CAST(mobileUsabilityVerdict AS VARCHAR) = 'PASS' THEN 1 ELSE 0 END)::BIGINT AS mobile_passes,
980
980
  SUM(CASE WHEN CAST(richResultsVerdict AS VARCHAR) = 'PASS' THEN 1 ELSE 0 END)::BIGINT AS rich_results_passes,
981
- SUM(CASE WHEN userCanonical IS NOT NULL AND googleCanonical IS NOT NULL AND CAST(userCanonical AS VARCHAR) <> CAST(googleCanonical AS VARCHAR) THEN 1 ELSE 0 END)::BIGINT AS canonical_mismatches
981
+ SUM(CASE WHEN canonicalMismatchKind IN ('path', 'cross_domain') THEN 1 ELSE 0 END)::BIGINT AS canonical_mismatches
982
982
  FROM read_parquet({{INSPECTIONS}}, union_by_name = true)
983
983
  WHERE substr(CAST(inspectedAt AS VARCHAR), 1, 10) >= '${utcDateMinusDays(windowAnchorMs, 90)}'
984
984
  GROUP BY 1
@@ -1,7 +1,7 @@
1
1
  import { ResolverAdapter } from "../resolver/types.mjs";
2
2
  import { PlannerCapabilities } from "gscdump/query/plan";
3
- import { BuilderState } from "gscdump/query";
4
3
  import { TableName } from "@gscdump/contracts";
4
+ import { BuilderState } from "gscdump/query";
5
5
  type QueryRow = Record<string, unknown>;
6
6
  interface FileSet {
7
7
  table: TableName;
@@ -1,8 +1,8 @@
1
1
  import { CompactionThresholds } from "./compaction.mjs";
2
2
  import { ParquetQueryFilter } from "./node_modules/.pnpm/hyparquet@1.26.2/node_modules/hyparquet/types/index.mjs";
3
3
  import "./node_modules/.pnpm/hyparquet@1.26.2/node_modules/hyparquet/types/node.mjs";
4
- import { BuilderState, SearchType, SearchType as SearchType$1 } from "gscdump/query";
5
4
  import { Grain, Grain as Grain$1, Row, Row as Row$1, TableName, TableName as TableName$1, TenantCtx, TenantCtx as TenantCtx$1 } from "@gscdump/contracts";
5
+ import { BuilderState, SearchType, SearchType as SearchType$1 } from "gscdump/query";
6
6
  interface WriteCtx extends TenantCtx {
7
7
  table: TableName;
8
8
  date?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine",
3
3
  "type": "module",
4
- "version": "1.4.7",
4
+ "version": "1.4.10",
5
5
  "description": "Append-only Parquet/DuckDB storage engine + planner + adapters for the gscdump pipeline. Node + edge runtimes; opt-in heavy peers.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -181,9 +181,9 @@
181
181
  "dependencies": {
182
182
  "drizzle-orm": "1.0.0-rc.3",
183
183
  "proper-lockfile": "^4.1.2",
184
- "@gscdump/contracts": "^1.4.7",
185
- "@gscdump/lakehouse": "^1.4.7",
186
- "gscdump": "^1.4.7"
184
+ "@gscdump/lakehouse": "^1.4.10",
185
+ "gscdump": "^1.4.10",
186
+ "@gscdump/contracts": "^1.4.10"
187
187
  },
188
188
  "devDependencies": {
189
189
  "@duckdb/duckdb-wasm": "1.33.1-dev57.0",