@nitpicker/archive 0.25.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +16 -16
  2. package/lib/archive.js +0 -1
  3. package/lib/create-entity-tables.d.ts +6 -6
  4. package/lib/create-entity-tables.js +6 -6
  5. package/lib/database.js +0 -1
  6. package/lib/db-ops/_shared/load-response-headers-by-set-ids.js +2 -2
  7. package/lib/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js +1 -1
  8. package/lib/db-ops/outages/close-stale-open-network-outages.js +2 -2
  9. package/lib/db-ops/outages/compute-outage-clamp-timestamp.js +2 -2
  10. package/lib/db-ops/outages/list-network-outages.js +1 -1
  11. package/lib/db-ops/templates/replace-page-templates.js +2 -2
  12. package/lib/filesystem/output-text.js +2 -2
  13. package/lib/filesystem/parse-pax-path.js +1 -3
  14. package/lib/filesystem/parse-tar-size-field.js +1 -4
  15. package/lib/filesystem/readline.js +1 -1
  16. package/lib/meta/derive-meta-extras.js +1 -1
  17. package/lib/meta/types.d.ts +1 -1
  18. package/lib/migrate-content-items-alias-of-id.js +1 -1
  19. package/lib/migrate-content-items-dedupe-cap-event-id.js +1 -1
  20. package/lib/migrate-content-items-is-metadata-only.js +1 -1
  21. package/lib/migrate-entity-tables.js +1 -1
  22. package/lib/migrate-info-created-cwd.js +1 -1
  23. package/lib/migrate-info-main-content-selector.js +1 -1
  24. package/lib/migrate-info-request-header-names.js +1 -1
  25. package/lib/migrate-info-roots.js +1 -1
  26. package/lib/migrate-inventory-runs-to-list-reconcile-runs.js +1 -1
  27. package/lib/migrate-list-reconcile-runs-exclude-skipped.js +1 -1
  28. package/lib/migrate-list-reconcile-runs-invalid-skipped.js +1 -1
  29. package/lib/migrate-main-contents-columns.js +1 -1
  30. package/lib/migrate-page-meta-body-hash.js +1 -1
  31. package/lib/migrate-page-meta-console-error-count.js +1 -1
  32. package/lib/migrate-page-meta-custom-element-count.js +1 -1
  33. package/lib/migrate-page-meta-image-scan.js +1 -1
  34. package/lib/migrate-page-tags-to-page-technologies.js +2 -2
  35. package/lib/migrate-ref-tables.js +1 -1
  36. package/lib/populate-entity-tables/populate-page-meta.js +1 -1
  37. package/lib/populate-ref-tables/populate-blob-refs.js +1 -1
  38. package/lib/template-classification/collect-page-ids-with-html.js +2 -2
  39. package/lib/template-classification/collect-page-stylesheet-urls.js +9 -7
  40. package/lib/template-classification/load-page-cluster-candidates.js +2 -2
  41. package/lib/test-utils/compare-strings.d.ts +12 -0
  42. package/lib/test-utils/compare-strings.js +14 -0
  43. package/lib/transfer/copy-page-template-labels.js +1 -1
  44. package/lib/transfer/list-transfer-columns.d.ts +6 -6
  45. package/lib/transfer/list-transfer-columns.js +6 -6
  46. package/lib/transfer/reclassify-resource-externality.d.ts +2 -2
  47. package/lib/transfer/reclassify-resource-externality.js +2 -2
  48. package/lib/utils/array/each-splitted.js +1 -1
  49. package/lib/utils/types/types.d.ts +1 -1
  50. package/package.json +5 -5
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @nitpicker/archive
2
2
 
3
- `.nitpicker` アーカイブ(tar + SQLite)の読み書きを担う内部パッケージです。スキーマ定義・マイグレーション・`Archive` / `ArchiveAccessor`・tar 展開キャッシュ・concat / split・body hash・URL alias・技術スタック検出のロールアップ・テンプレート分類・保存済み HTML スナップショットのセレクタ照合と文字列・正規表現検索(`html-snapshot/`。ネイティブの `@nitpicker/core` を呼ぶ薄いラッパーと、その型)・スコープ判定・エラー分類を含みます。
3
+ `.nitpicker`アーカイブ(tar + SQLite)の読み書きを担う内部パッケージです。スキーマ定義・マイグレーション・`Archive`/`ArchiveAccessor`・tar 展開キャッシュ・concat/split・body hash・URL alias・技術スタック検出のロールアップ・テンプレート分類・保存済み HTML スナップショットのセレクタ照合と文字列・正規表現検索(`html-snapshot/`。ネイティブの`@nitpicker/core`を呼ぶ薄いラッパーと、その型)・スコープ判定・エラー分類を含みます。
4
4
 
5
- ヘッドレスブラウザ(puppeteer / `@d-zero/beholder`)や `@d-zero/dealer` には依存しないため、読み取り専用の経路([@nitpicker/query](../query/README.md)、viewer、mcp-server、report-\*)はクローラーを引き込まずにアーカイブを扱えます。クロール時の書き込みは [@nitpicker/crawler](../crawler/README.md) がこのパッケージ経由で行います。
5
+ ヘッドレスブラウザ(puppeteer/`@d-zero/beholder`)や`@d-zero/dealer`には依存しないため、読み取り専用の経路([@nitpicker/query](../query/README.md)、viewer、mcp-server、report-\*)はクローラーを引き込まずにアーカイブを扱えます。クロール時の書き込みは[@nitpicker/crawler](../crawler/README.md)がこのパッケージ経由で行います。
6
6
 
7
7
  ## 使い方
8
8
 
9
- 公開 API は機能単位のサブパスで export しています(パッケージルート `.` の export はありません)。
9
+ 公開 API は機能単位のサブパスで export しています(パッケージルート`.`の export はありません)。
10
10
 
11
11
  ```ts
12
12
  import Archive from '@nitpicker/archive/archive';
@@ -19,17 +19,17 @@ const config = await accessor.getConfig();
19
19
  console.log(classifyErrorKind('net::ERR_NAME_NOT_RESOLVED')); // 'dns'
20
20
  ```
21
21
 
22
- 利用可能なサブパスは `package.json` の `exports` を参照してください。
22
+ 利用可能なサブパスは`package.json`の`exports`を参照してください。
23
23
 
24
24
  ## テンプレート分類(page-cluster)
25
25
 
26
- テンプレート分類(`src/template-classification/`)は、ページを DOM 構造の類似性でクラスタリングします。計算はすべて [@nitpicker/core](../core/README.md) のネイティブアドオン(Rust の `nitpicker_template_cluster`)が実行し、`src/template-classification/page-cluster/` にはそれを駆動するドライバ(`resolve-page-cluster-keys.ts` の `resolvePageClusterKeys`)だけがあります。ドライバはページのシグナル(URL パス・スタイルシート・ホスト)を渡してから、各ページの HTML を必要な読み込みでだけ取り出して渡し、ページごとのキーとクラスタごとの reason を受け取ります。ドライバはこのディレクトリの外(`@nitpicker/core` を除く)に依存せず、パッケージの `exports` にも載せていません。呼び出しは `classify-page-templates.ts` / `create-page-cluster-factory.ts` に閉じています(ほかはテスト専用の `golden/`)。分類器の `clusterKey` は nitpicker 側で `templateKey` と呼び替えます。
26
+ テンプレート分類(`src/template-classification/`)は、ページを DOM 構造の類似性でクラスタリングします。計算はすべて[@nitpicker/core](../core/README.md)のネイティブアドオン(Rust の`nitpicker_template_cluster`)が実行し、`src/template-classification/page-cluster/`にはそれを駆動するドライバ(`resolve-page-cluster-keys.ts`の`resolvePageClusterKeys`)だけがあります。ドライバはページのシグナル(URL パス・スタイルシート・ホスト)を渡してから、各ページの HTML を必要な読み込みでだけ取り出して渡し、ページごとのキーとクラスタごとの reason を受け取ります。ドライバはこのディレクトリの外(`@nitpicker/core`を除く)に依存せず、パッケージの`exports`にも載せていません。呼び出しは`classify-page-templates.ts`/`create-page-cluster-factory.ts`に閉じています(ほかはテスト専用の`golden/`)。分類器の`clusterKey`は nitpicker 側で`templateKey`と呼び替えます。
27
27
 
28
- 出力は既存アーカイブに保存済みの `templateKey` と reason にバイト単位で一致し、`src/template-classification/golden/` の golden がそれを固定しています。
28
+ 出力は既存アーカイブに保存済みの`templateKey`と reason にバイト単位で一致し、`src/template-classification/golden/`の golden がそれを固定しています。
29
29
 
30
30
  ### アルゴリズム概観
31
31
 
32
- `clusterKey` がどう決まるかを知っておくと、分類結果の解釈(なぜこの 2 ページが同じ `templateKey` なのか)がしやすくなります。実装詳細の WHY は `nitpicker_template_cluster` の各モジュールの doc comment が正です。
32
+ `clusterKey`がどう決まるかを知っておくと、分類結果の解釈(なぜこの 2 ページが同じ`templateKey`なのか)がしやすくなります。実装詳細の WHY は`nitpicker_template_cluster`の各モジュールの doc comment が正です。
33
33
 
34
34
  #### 全体パイプライン
35
35
 
@@ -52,8 +52,8 @@ flowchart TD
52
52
 
53
53
  - **Pass 0(ブロッキング)** — HTML を読まず、URL パスと first-party stylesheet 集合だけで粗く分割します。高価な構造比較を同一ブロック内に閉じ込め、コーパス全体の比較コストを O(n²) から劇的に減らします。stylesheet を持たない orphan ページは同一セクションの CSS ブロックへ再割当されます
54
54
  - **chrome discovery** — 全ページのランドマーク署名の度数分布に auto-cut を当て、閾値以上を「グローバル chrome」(サイト共通のヘッダー等)として比較から除外し、閾値未満かつ 2 ページ以上に出現するものを「ローカル chrome」(セクション固有のナビ等)としてトークン再注入します
55
- - **Stage A(ブロック内クラスタリング)** — ブロックごとに直線的に処理します。本文ルート(`contentRoot` → `<main>` → 内蔵リスト)の深さキャップ(候補深度 2〜10 を全走査して knee を探す自動選択)→ tokenize → **ページ固有クラスの除去**(ブロック内で 1 ページにしか現れないクラスは `<article class="outline">` のようなページ識別ラベルであってテンプレート構造ではないので、`allowedClasses` で除いて再 tokenize します。10 ページ以上のブロックのみ)→ complete-linkage 階層クラスタリング → max-gap auto-cut でカット高を決定 → 最後に包含関係にあるクラスタを吸収する包含割当(割当チェーンを辿り、循環はメンバー最大のクラスタをルートに選んで解決)
56
- - **Pass 1b(サンプルに落としたブロックのみ)** — Stage A の類似度表がメモリ上限(`--template-memory-budget <MiB>` または `NITPICKER_TEMPLATE_MEMORY_BUDGET`、既定 512 MiB、0 なら全ブロック)を超えるブロックだけ、リザーバサンプリング(最大 100 ページ、ブロックキーをシードにした決定的乱数)で代表させ、サンプル外のページは Stage A 完了後に max-Jaccard で最寄りクラスタへ一括割当する(tokenize にはサンプルで学習した許可クラス集合を適用し、サンプル側と同じ規則で比較します)。そのブロックのメモリ使用量はブロックのページ数ではなくサンプルサイズに比例します。もう 1 つの上限は、全ページで計算するブロックのページ(HTML の解析結果を計算が終わるまで全ブロック分抱える。1 ページ約 60KB)の合計で、`@nitpicker/core` の `TEMPLATE_EXACT_PAGE_LIMIT`(20,000 ページ、固定)です。超えるときは、標本より大きい(100 ページを超える)ブロックを、ページ数が多い順(同数ならブロックの出現順)に、収まるまでサンプルに落とします。100 ページ以下のブロックはサンプルにしても保持ページが減らないので落とさず、それだけで上限を超えるときは超えたまま保持します。ブロックの判定はページ数だけで決まり(重複する集合のまとめ込みはしません)、判定したブロックは `block-sampled` の進捗イベントと分類結果の `sampledBlocks` に、理由(`table-over-budget`、`pages-over-limit`)つきで残ります。サンプルに落としたブロックを含むマージ済みグループは Stage B でも 100 メンバーに抑えます
55
+ - **Stage A(ブロック内クラスタリング)** — ブロックごとに直線的に処理します。本文ルート(`contentRoot` → `<main>` → 内蔵リスト)の深さキャップ(候補深度 2〜10 を全走査して knee を探す自動選択)→ tokenize → **ページ固有クラスの除去**(ブロック内で 1 ページにしか現れないクラスは`<article class="outline">`のようなページ識別ラベルであってテンプレート構造ではないので、`allowedClasses`で除いて再 tokenize します。10 ページ以上のブロックのみ)→ complete-linkage 階層クラスタリング → max-gap auto-cut でカット高を決定 → 最後に包含関係にあるクラスタを吸収する包含割当(割当チェーンを辿り、循環はメンバー最大のクラスタをルートに選んで解決)
56
+ - **Pass 1b(サンプルに落としたブロックのみ)** — Stage A の類似度表がメモリ上限(`--template-memory-budget <MiB>`または`NITPICKER_TEMPLATE_MEMORY_BUDGET`、既定 512 MiB、0 なら全ブロック)を超えるブロックだけ、リザーバサンプリング(最大 100 ページ、ブロックキーをシードにした決定的乱数)で代表させ、サンプル外のページは Stage A 完了後に max-Jaccard で最寄りクラスタへ一括割当する(tokenize にはサンプルで学習した許可クラス集合を適用し、サンプル側と同じ規則で比較します)。そのブロックのメモリ使用量はブロックのページ数ではなくサンプルサイズに比例します。もう 1 つの上限は、全ページで計算するブロックのページ(HTML の解析結果を計算が終わるまで全ブロック分抱える。1 ページ約 60KB)の合計で、`@nitpicker/core`の`TEMPLATE_EXACT_PAGE_LIMIT`(20,000 ページ、固定)です。超えるときは、標本より大きい(100 ページを超える)ブロックを、ページ数が多い順(同数ならブロックの出現順)に、収まるまでサンプルに落とします。100 ページ以下のブロックはサンプルにしても保持ページが減らないので落とさず、それだけで上限を超えるときは超えたまま保持します。ブロックの判定はページ数だけで決まり(重複する集合のまとめ込みはしません)、判定したブロックは`block-sampled`の進捗イベントと分類結果の`sampledBlocks`に、理由(`table-over-budget`、`pages-over-limit`)つきで残ります。サンプルに落としたブロックを含むマージ済みグループは Stage B でも 100 メンバーに抑えます
57
57
  - **Stage B(ブロック越えマージ)** — ブロック分割はあくまで比較コスト削減のためなので、最後に同一テンプレートがブロックを跨いで分かれていないか再統合します。これが唯一の反復処理です(次節)。収束後、確定した最終クラスタごとに reason(`TemplateClusterReason`)を 1 つずつ組み立てます — 追加の全コーパススキャンではなく、Stage A/B が既に計算済みの中間データ(quorum core、landmark インスタンス、ブロッキング根拠)を再利用するだけなので、クラスタ数にしか比例しません
58
58
 
59
59
  #### Stage B: ブロック越え統合の不動点ループ
@@ -74,27 +74,27 @@ flowchart TD
74
74
 
75
75
  マージが起きるとユニットのメンバー構成が変わり、文書頻度も quorum core も変わります。そのため毎ラウンド、統合後のプールから全指標を**再計算**してマージを再試行します。fine stage・L2 stage の両方でマージが 1 件も出なくなった時点で不動点に到達したとみなして収束します(安全弁として最大 10 ラウンド。実データでは 7 ラウンド以内に収束)。L2 stage は fine stage が空振りしたラウンドでしか実行されない最後の粗い経路で、誤マージ防止のために shell(ランドマーク由来トークン)の相互裏付けを要求します。
76
76
 
77
- fine stage・L2 stage いずれの経路で提案されたマージも、適用前に**凝集度ガード**を通ります: 統合後の quorum core が統合前の core に対して一定比率を下回るなら、そのマージは破棄されます。個々の経路が「統合前のペア類似度」だけを見て提案する一方、複数ラウンドにわたる連鎖的な統合は「統合後に実際どれだけまとまっているか」を悪化させ得る(無関係なテンプレート同士が少しずつ吸収し合う catch-all 化)ため、経路をまたいだ単一のチェックポイントとして機能します。あわせて、L2 stage 自体もラウンド開始前に判別力を検査し、参加ユニット全体が同一の署名形状に潰れている(`main` 直下の浅い階層しか手がかりが残っていない等)場合は、そのラウンドの L2 比較を丸ごとスキップします。詳細は `merge_cross_block_clusters.rs` / `filter_merges_by_cohesion.rs` / `l2_signature.rs` の doc comment を参照してください。
77
+ fine stage・L2 stage いずれの経路で提案されたマージも、適用前に**凝集度ガード**を通ります: 統合後の quorum core が統合前の core に対して一定比率を下回るなら、そのマージは破棄されます。個々の経路が「統合前のペア類似度」だけを見て提案する一方、複数ラウンドにわたる連鎖的な統合は「統合後に実際どれだけまとまっているか」を悪化させ得る(無関係なテンプレート同士が少しずつ吸収し合う catch-all 化)ため、経路をまたいだ単一のチェックポイントとして機能します。あわせて、L2 stage 自体もラウンド開始前に判別力を検査し、参加ユニット全体が同一の署名形状に潰れている(`main`直下の浅い階層しか手がかりが残っていない等)場合は、そのラウンドの L2 比較を丸ごとスキップします。詳細は`merge_cross_block_clusters.rs`/`filter_merges_by_cohesion.rs`/`l2_signature.rs`の doc comment を参照してください。
78
78
 
79
- 凝集度ガードの前に、Stage A が同一ブロック内で別クラスタと判定した(双方 3 ページ以上の)ユニット同士を、構造トークンに実質的な差がある限り統合しない**分離尊重ガード**も通ります。生の core では閾値に届かないが、クラス名を除くと骨格が一致する組(BEM 名だけが違う同一骨格)は対象外です。詳細は `filter_merges_by_stage_a_separation.rs` の doc comment を参照してください。
79
+ 凝集度ガードの前に、Stage A が同一ブロック内で別クラスタと判定した(双方 3 ページ以上の)ユニット同士を、構造トークンに実質的な差がある限り統合しない**分離尊重ガード**も通ります。生の core では閾値に届かないが、クラス名を除くと骨格が一致する組(BEM 名だけが違う同一骨格)は対象外です。詳細は`filter_merges_by_stage_a_separation.rs`の doc comment を参照してください。
80
80
 
81
81
  #### Self-tuning
82
82
 
83
- 閾値の多くは **max-gap auto-cut**(度数分布の隣接ギャップ最大の中点を境界とする)でデータから自己発見されます。① Stage A のカット高、② Stage B の shell 判定、③ chrome discovery のグローバル/ローカル判定、の 3 箇所で同一プリミティブを再利用しているので、サイトごとにハイパーパラメータをチューニングする必要はありません。詳細は `auto_cut_threshold.rs` の doc comment を参照してください。例外的に Stage B fine stage の complete-linkage だけは固定閾値 0.8 を使います(理由は `merge_cross_block_clusters.rs` を参照)。
83
+ 閾値の多くは **max-gap auto-cut**(度数分布の隣接ギャップ最大の中点を境界とする)でデータから自己発見されます。① Stage A のカット高、② Stage B の shell 判定、③ chrome discovery のグローバル/ローカル判定、の 3 箇所で同一プリミティブを再利用しているので、サイトごとにハイパーパラメータをチューニングする必要はありません。詳細は`auto_cut_threshold.rs`の doc comment を参照してください。例外的に Stage B fine stage の complete-linkage だけは固定閾値 0.8 を使います(理由は`merge_cross_block_clusters.rs`を参照)。
84
84
 
85
85
  ### golden(出力を固定するテスト)
86
86
 
87
- `src/template-classification/golden/` は、分類器の出力(ページごとの `templateKey` と、`page_template_clusters` に保存される reason JSON の文字列)を `__golden__/` に固定し、全件一致を確かめます。入力は、決定的に生成する合成サイトと `page-cluster/__fixtures__/` の HTML で、ドライバに直接渡します。合成サイトは、小さい 1 規模と、約 2 万ページ強の 1 規模です。後者はメモリ上限を 3 通りに変えて記録します。既定のメモリ上限では最大のブロックだけが保持ページ数の上限でサンプルに落ち、メモリ上限 0 では全ブロックが落ち、メモリ上限を絞った場合は一部のブロックだけが落ちます。メモリ上限 0 の記録は、全ブロックをサンプルから計算した結果で、この方式の出力を固定します。分類器を書き換えても出力が同じであることのゲートです。アーカイブ側のアダプタ(`createPageClusterFactory` など)を含む本番経路全体は、実アーカイブで `scripts/template-classification-golden.mjs` を使って確かめます。
87
+ `src/template-classification/golden/`は、分類器の出力(ページごとの`templateKey`と、`page_template_clusters`に保存される reason JSON の文字列)を`__golden__/`に固定し、全件一致を確かめます。入力は、決定的に生成する合成サイトと`page-cluster/__fixtures__/`の HTML で、ドライバに直接渡します。合成サイトは、小さい 1 規模と、約 2 万ページ強の 1 規模です。後者はメモリ上限を 3 通りに変えて記録します。既定のメモリ上限では最大のブロックだけが保持ページ数の上限でサンプルに落ち、メモリ上限 0 では全ブロックが落ち、メモリ上限を絞った場合は一部のブロックだけが落ちます。メモリ上限 0 の記録は、全ブロックをサンプルから計算した結果で、この方式の出力を固定します。分類器を書き換えても出力が同じであることのゲートです。アーカイブ側のアダプタ(`createPageClusterFactory`など)を含む本番経路全体は、実アーカイブで`scripts/template-classification-golden.mjs`を使って確かめます。
88
88
 
89
- 出力を意図して変えるときだけ、次のコマンドで `__golden__/` を更新し、理由を PR に書きます(パスを付けないと他の spec の snapshot まで更新されます)。
89
+ 出力を意図して変えるときだけ、次のコマンドで`__golden__/`を更新し、理由を PR に書きます(パスを付けないと他の spec の snapshot まで更新されます)。
90
90
 
91
91
  ```sh
92
92
  yarn vitest run packages/@nitpicker/archive/src/template-classification/golden --exclude '.claude/**' -u
93
93
  ```
94
94
 
95
- 差分を読むときは `inputDigest` を先に見ます。`inputDigest` が変わっていれば入力(生成器か fixture)が変わっています。`inputDigest` が同じなのにほかのフィールドが変わっていれば、分類器の出力が変わっています。
95
+ 差分を読むときは`inputDigest`を先に見ます。`inputDigest`が変わっていれば入力(生成器か fixture)が変わっています。`inputDigest`が同じなのにほかのフィールドが変わっていれば、分類器の出力が変わっています。
96
96
 
97
- `page-cluster/__fixtures__/` の HTML は、`@nitpicker/core` の Rust テスト(htmlparser2 との突き合わせ)も読みます。fixture を足したり変えたりしたら、core の `scripts/record-htmlparser2-events.mjs` で記録済みイベントを取り直します(手順はスクリプト冒頭)。取り直すまで core のテストが落ちます。
97
+ `page-cluster/__fixtures__/`の HTML は、`@nitpicker/core`の Rust テスト(htmlparser2 との突き合わせ)も読みます。fixture を足したり変えたりしたら、core の`scripts/record-htmlparser2-events.mjs`で記録済みイベントを取り直します(手順はスクリプト冒頭)。取り直すまで core のテストが落ちます。
98
98
 
99
99
  ## 関連リンク
100
100
 
package/lib/archive.js CHANGED
@@ -74,7 +74,6 @@ export default class Archive extends ArchiveAccessor {
74
74
  get renamedDir() {
75
75
  return path.resolve(path.dirname(this.#filePath), path.basename(this.#filePath, path.extname(this.#filePath)));
76
76
  }
77
- // eslint-disable-next-line no-restricted-syntax
78
77
  constructor(filePath, tmpDir, db, releaseLock) {
79
78
  super(tmpDir, db);
80
79
  this.#filePath = filePath;
@@ -106,8 +106,8 @@ import type { Knex } from 'knex';
106
106
  * for the pair); duplicate observations are counted via `count` without
107
107
  * retaining their bodies. The UNIQUE constraint's auto-index on
108
108
  * `(page_id, href_page_id)` also serves any `WHERE page_id = ?` seek via
109
- * SQLite's leading-prefix rule, so no separate `CREATE INDEX ... ON
110
- * anchor_edges(page_id)` is needed; only the reverse-direction
109
+ * SQLite's leading-prefix rule, so no separate
110
+ * `CREATE INDEX ... ON anchor_edges(page_id)` is needed; only the reverse-direction
111
111
  * `idx_anchor_edges_href` on `href_page_id` alone is added.
112
112
  *
113
113
  * **`image_items.dom_path_text_id NOT NULL`.** Every image row must
@@ -127,8 +127,8 @@ import type { Knex } from 'knex';
127
127
  * and the viewer would arbitrarily pick one to render.
128
128
  *
129
129
  * **`resource_items` url / blob mutual-exclusion CHECK.** Same rationale
130
- * as the image CHECK above, but stricter: `(url_id IS NULL) !=
131
- * (url_blob_id IS NULL)` requires EXACTLY one slot to be non-null (not
130
+ * as the image CHECK above, but stricter:
131
+ * `(url_id IS NULL) != (url_blob_id IS NULL)` requires EXACTLY one slot to be non-null (not
132
132
  * "at most one") because a resource — unlike an `<img>` that can
133
133
  * legitimately have neither `src` nor `currentSrc` — is always
134
134
  * identified by some URL. `resolveUrlOrBlob` (writer) /
@@ -178,8 +178,8 @@ import type { Knex } from 'knex';
178
178
  *
179
179
  * ### Idempotency
180
180
  *
181
- * All statements use `CREATE TABLE IF NOT EXISTS` / `CREATE INDEX IF NOT
182
- * EXISTS` so the primitive is safe to call multiple times against the
181
+ * All statements use `CREATE TABLE IF NOT EXISTS` /
182
+ * `CREATE INDEX IF NOT EXISTS` so the primitive is safe to call multiple times against the
183
183
  * same DB. Both call sites (`initSchema` and
184
184
  * `migrateEntityTables`) still guard on their own sentinels,
185
185
  * but the function itself no longer depends on the sentinel being
@@ -105,8 +105,8 @@
105
105
  * for the pair); duplicate observations are counted via `count` without
106
106
  * retaining their bodies. The UNIQUE constraint's auto-index on
107
107
  * `(page_id, href_page_id)` also serves any `WHERE page_id = ?` seek via
108
- * SQLite's leading-prefix rule, so no separate `CREATE INDEX ... ON
109
- * anchor_edges(page_id)` is needed; only the reverse-direction
108
+ * SQLite's leading-prefix rule, so no separate
109
+ * `CREATE INDEX ... ON anchor_edges(page_id)` is needed; only the reverse-direction
110
110
  * `idx_anchor_edges_href` on `href_page_id` alone is added.
111
111
  *
112
112
  * **`image_items.dom_path_text_id NOT NULL`.** Every image row must
@@ -126,8 +126,8 @@
126
126
  * and the viewer would arbitrarily pick one to render.
127
127
  *
128
128
  * **`resource_items` url / blob mutual-exclusion CHECK.** Same rationale
129
- * as the image CHECK above, but stricter: `(url_id IS NULL) !=
130
- * (url_blob_id IS NULL)` requires EXACTLY one slot to be non-null (not
129
+ * as the image CHECK above, but stricter:
130
+ * `(url_id IS NULL) != (url_blob_id IS NULL)` requires EXACTLY one slot to be non-null (not
131
131
  * "at most one") because a resource — unlike an `<img>` that can
132
132
  * legitimately have neither `src` nor `currentSrc` — is always
133
133
  * identified by some URL. `resolveUrlOrBlob` (writer) /
@@ -177,8 +177,8 @@
177
177
  *
178
178
  * ### Idempotency
179
179
  *
180
- * All statements use `CREATE TABLE IF NOT EXISTS` / `CREATE INDEX IF NOT
181
- * EXISTS` so the primitive is safe to call multiple times against the
180
+ * All statements use `CREATE TABLE IF NOT EXISTS` /
181
+ * `CREATE INDEX IF NOT EXISTS` so the primitive is safe to call multiple times against the
182
182
  * same DB. Both call sites (`initSchema` and
183
183
  * `migrateEntityTables`) still guard on their own sentinels,
184
184
  * but the function itself no longer depends on the sentinel being
package/lib/database.js CHANGED
@@ -100,7 +100,6 @@ export class Database extends EventEmitter {
100
100
  #instance;
101
101
  /** Connection-scoped write-side id caches for entity/ref upserts. */
102
102
  #writeRefCaches = createWriteRefCaches();
103
- // eslint-disable-next-line no-restricted-syntax
104
103
  constructor(options) {
105
104
  super();
106
105
  // **Known caveat (libsql 0.5.x)**: passing `readonly: true` via
@@ -31,12 +31,12 @@ export async function loadResponseHeadersBySetIds(knex, headerSetIds) {
31
31
  const merged = new Map();
32
32
  for (let index = 0; index < headerSetIds.length; index += LOOKUP_CHUNK_SIZE) {
33
33
  const chunk = headerSetIds.slice(index, index + LOOKUP_CHUNK_SIZE);
34
- const headerRows = (await knex('header_set_entries as hse')
34
+ const headerRows = await knex('header_set_entries as hse')
35
35
  .join('header_name_refs as hnr', 'hnr.id', 'hse.name_id')
36
36
  .join('header_value_refs as hvr', 'hvr.id', 'hse.value_id')
37
37
  .whereIn('hse.header_set_id', chunk)
38
38
  .orderBy(['hse.header_set_id', 'hnr.name', 'hse.occurrence'])
39
- .select('hse.header_set_id as headerSetId', 'hnr.name as name', 'hvr.value as value'));
39
+ .select('hse.header_set_id as headerSetId', 'hnr.name as name', 'hvr.value as value');
40
40
  for (const row of headerRows) {
41
41
  const bySet = merged.get(row.headerSetId) ?? new Map();
42
42
  const values = bySet.get(row.name) ?? [];
@@ -22,6 +22,6 @@ export async function listDedupeCapShapeKeys(knex) {
22
22
  if (!hasTable) {
23
23
  return [];
24
24
  }
25
- const rows = (await knex('dedupe_cap_events').distinct('shape_key'));
25
+ const rows = await knex('dedupe_cap_events').distinct('shape_key');
26
26
  return rows.map((row) => row.shape_key);
27
27
  }
@@ -29,9 +29,9 @@ export async function closeStaleOpenNetworkOutages(knex) {
29
29
  if (!hasTable) {
30
30
  return;
31
31
  }
32
- const openRows = (await knex('network_outages')
32
+ const openRows = await knex('network_outages')
33
33
  .whereNull('ended_at')
34
- .select('id', 'started_at'));
34
+ .select('id', 'started_at');
35
35
  if (openRows.length === 0) {
36
36
  return;
37
37
  }
@@ -23,8 +23,8 @@
23
23
  * (a brand-new archive with no activity yet).
24
24
  */
25
25
  export async function computeOutageClampTimestamp(knex) {
26
- const [crawlErrorsRow] = (await knex('crawl_errors').max('createdAt as maxCreatedAt'));
27
- const [contentItemsRow] = (await knex('content_items').max('last_crawled_at as maxLastCrawledAt'));
26
+ const [crawlErrorsRow] = await knex('crawl_errors').max('createdAt as maxCreatedAt');
27
+ const [contentItemsRow] = await knex('content_items').max('last_crawled_at as maxLastCrawledAt');
28
28
  const latestError = crawlErrorsRow?.maxCreatedAt ?? 0;
29
29
  const latestCrawl = contentItemsRow?.maxLastCrawledAt ?? 0;
30
30
  return Math.max(latestError, latestCrawl);
@@ -29,7 +29,7 @@ export async function listNetworkOutages(knex) {
29
29
  if (!hasTable) {
30
30
  return [];
31
31
  }
32
- const rows = (await knex('network_outages').select('started_at', 'ended_at'));
32
+ const rows = await knex('network_outages').select('started_at', 'ended_at');
33
33
  if (rows.length === 0) {
34
34
  return [];
35
35
  }
@@ -147,7 +147,7 @@ function labelPair(label) {
147
147
  * @returns The previous membership; empty on a first run.
148
148
  */
149
149
  async function loadPreviousMembership(trx) {
150
- const previousRows = (await trx('page_templates').select('page_id as pageId', 'template_key as templateKey'));
150
+ const previousRows = await trx('page_templates').select('page_id as pageId', 'template_key as templateKey');
151
151
  const membership = new Map();
152
152
  for (const row of previousRows) {
153
153
  const pageIds = membership.get(row.templateKey);
@@ -166,7 +166,7 @@ async function loadPreviousMembership(trx) {
166
166
  * @returns Template key → label; empty on a first run.
167
167
  */
168
168
  async function loadPreviousLabels(trx) {
169
- const labelRows = (await trx('page_template_labels').select('template_key as templateKey', 'section', 'ordinal'));
169
+ const labelRows = await trx('page_template_labels').select('template_key as templateKey', 'section', 'ordinal');
170
170
  return new Map(labelRows.map((row) => [
171
171
  row.templateKey,
172
172
  { section: row.section, ordinal: row.ordinal },
@@ -16,13 +16,13 @@ export async function outputText(filePath, data) {
16
16
  mkdir(filePath);
17
17
  await fs.writeFile(filePath, data, { encoding: 'utf8' }).catch(async (error) => {
18
18
  if (error instanceof Error && 'code' in error && error.code === 'ENAMETOOLONG') {
19
- // eslint-disable-next-line no-console
19
+ // oxlint-disable-next-line no-console
20
20
  console.error(`File path too long: ${filePath}`);
21
21
  const dir = path.dirname(filePath);
22
22
  const altFileName = `__file_path_too_long_${(filePathTooLongCount++).toString().padStart(4, '0')}`;
23
23
  const ext = path.extname(filePath);
24
24
  const altFilePath = path.resolve(dir, `${altFileName}${ext}`);
25
- // eslint-disable-next-line no-console
25
+ // oxlint-disable-next-line no-console
26
26
  console.error(`Try to save to: ${altFilePath}`);
27
27
  const altMetaFilePath = path.resolve(dir, `${altFileName}.meta.txt`);
28
28
  await outputText(altFilePath, data);
@@ -31,9 +31,7 @@ export function parsePaxPath(data) {
31
31
  if (recordEnd > data.length) {
32
32
  return null;
33
33
  }
34
- // Decimal (not `0x3D`) to sidestep the Prettier/eslint hex-case
35
- // conflict — see `parse-tar-size-field.ts` for the full explanation.
36
- const equalsIndex = data.indexOf(61 /* '=' */, spaceIndex);
34
+ const equalsIndex = data.indexOf(0x3d /* '=' */, spaceIndex);
37
35
  if (equalsIndex !== -1 && equalsIndex < recordEnd) {
38
36
  const key = data.subarray(spaceIndex + 1, equalsIndex).toString('utf8');
39
37
  if (key === 'path') {
@@ -21,10 +21,7 @@ export function parseTarSizeField(field) {
21
21
  // tars never approach sizes needing this (single-digit-terabyte
22
22
  // range before it would even matter), but a correct read here still
23
23
  // costs nothing.
24
- // `0b0111_1111` (not `0x7F`) to sidestep the Prettier/eslint hex-case
25
- // conflict (Prettier always lowercases hex digits; this repo's
26
- // `unicorn/number-literal-case` requires uppercase) — a binary literal
27
- // has no letter digits to disagree about.
24
+ // Mask off the high bit that marks the base-256 form.
28
25
  let value = BigInt(first & 0b0111_1111);
29
26
  for (let i = 1; i < field.length; i++) {
30
27
  value = (value << 8n) | BigInt(field[i] ?? 0);
@@ -16,7 +16,7 @@ export async function readline(filePath, callback) {
16
16
  const promiseBuffer = [];
17
17
  await new Promise((resolve) => {
18
18
  rLine.on('line', (line) => {
19
- promiseBuffer.push(callback(line));
19
+ promiseBuffer.push(Promise.resolve(callback(line)));
20
20
  });
21
21
  rLine.on('close', () => {
22
22
  resolve();
@@ -17,7 +17,7 @@
17
17
  * @returns Plain object suitable for `JSON.stringify`.
18
18
  */
19
19
  export function deriveMetaExtras(meta) {
20
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- we destructure to omit these from the spread
20
+ // oxlint-disable-next-line typescript/no-unused-vars -- we destructure to omit these from the spread
21
21
  const { jsonLd, speculationRules, tags, _raw, ...rest } = meta;
22
22
  return rest;
23
23
  }
@@ -442,7 +442,7 @@ export interface JsonLdRow {
442
442
  /** Original script text content (uncompressed; SQLite overflow pages handle large rows). */
443
443
  raw: string;
444
444
  /** Parsed JSON object (`null` when `parseError` is set). */
445
- parsed: unknown | null;
445
+ parsed: unknown;
446
446
  /** Parse error message preserved from beholder; `null` when the entry parsed cleanly. */
447
447
  parseError: string | null;
448
448
  }
@@ -61,7 +61,7 @@ export async function migrateContentItemsAliasOfId(instance, onLog) {
61
61
  onLog(message);
62
62
  }
63
63
  else {
64
- // eslint-disable-next-line no-console
64
+ // oxlint-disable-next-line no-console
65
65
  console.error(message);
66
66
  }
67
67
  }
@@ -55,7 +55,7 @@ export async function migrateContentItemsDedupeCapEventId(instance, onLog) {
55
55
  onLog(message);
56
56
  }
57
57
  else {
58
- // eslint-disable-next-line no-console
58
+ // oxlint-disable-next-line no-console
59
59
  console.error(message);
60
60
  }
61
61
  }
@@ -53,7 +53,7 @@ export async function migrateContentItemsIsMetadataOnly(instance, onLog) {
53
53
  onLog(message);
54
54
  }
55
55
  else {
56
- // eslint-disable-next-line no-console
56
+ // oxlint-disable-next-line no-console
57
57
  console.error(message);
58
58
  }
59
59
  }
@@ -61,6 +61,6 @@ export async function migrateEntityTables(instance) {
61
61
  return;
62
62
  }
63
63
  await createEntityTables(instance);
64
- // eslint-disable-next-line no-console
64
+ // oxlint-disable-next-line no-console
65
65
  console.error('[migrate] 0.13 entity/edge tables created');
66
66
  }
@@ -30,7 +30,7 @@ export async function migrateInfoCreatedCwd(instance, onLog) {
30
30
  onLog(message);
31
31
  }
32
32
  else {
33
- // eslint-disable-next-line no-console
33
+ // oxlint-disable-next-line no-console
34
34
  console.error(message);
35
35
  }
36
36
  }
@@ -29,7 +29,7 @@ export async function migrateInfoMainContentSelector(instance, onLog) {
29
29
  onLog(message);
30
30
  }
31
31
  else {
32
- // eslint-disable-next-line no-console
32
+ // oxlint-disable-next-line no-console
33
33
  console.error(message);
34
34
  }
35
35
  }
@@ -29,7 +29,7 @@ export async function migrateInfoRequestHeaderNames(instance, onLog) {
29
29
  onLog(message);
30
30
  }
31
31
  else {
32
- // eslint-disable-next-line no-console
32
+ // oxlint-disable-next-line no-console
33
33
  console.error(message);
34
34
  }
35
35
  }
@@ -46,7 +46,7 @@ export async function migrateInfoRoots(instance, onLog) {
46
46
  onLog(message);
47
47
  }
48
48
  else {
49
- // eslint-disable-next-line no-console
49
+ // oxlint-disable-next-line no-console
50
50
  console.error(message);
51
51
  }
52
52
  }
@@ -45,7 +45,7 @@ export async function migrateInventoryRunsToListReconcileRuns(instance, onLog) {
45
45
  onLog(message);
46
46
  }
47
47
  else {
48
- // eslint-disable-next-line no-console
48
+ // oxlint-disable-next-line no-console
49
49
  console.error(message);
50
50
  }
51
51
  }
@@ -38,7 +38,7 @@ export async function migrateListReconcileRunsExcludeSkipped(instance, onLog) {
38
38
  onLog(message);
39
39
  }
40
40
  else {
41
- // eslint-disable-next-line no-console
41
+ // oxlint-disable-next-line no-console
42
42
  console.error(message);
43
43
  }
44
44
  }
@@ -29,7 +29,7 @@ export async function migrateListReconcileRunsInvalidSkipped(instance, onLog) {
29
29
  onLog(message);
30
30
  }
31
31
  else {
32
- // eslint-disable-next-line no-console
32
+ // oxlint-disable-next-line no-console
33
33
  console.error(message);
34
34
  }
35
35
  }
@@ -55,7 +55,7 @@ export async function migrateMainContentsColumns(instance, onLog) {
55
55
  onLog(message);
56
56
  }
57
57
  else {
58
- // eslint-disable-next-line no-console
58
+ // oxlint-disable-next-line no-console
59
59
  console.error(message);
60
60
  }
61
61
  }
@@ -55,7 +55,7 @@ export async function migratePageMetaBodyHash(instance, onLog) {
55
55
  onLog(message);
56
56
  }
57
57
  else {
58
- // eslint-disable-next-line no-console
58
+ // oxlint-disable-next-line no-console
59
59
  console.error(message);
60
60
  }
61
61
  }
@@ -38,7 +38,7 @@ export async function migratePageMetaConsoleErrorCount(instance, onLog) {
38
38
  onLog(message);
39
39
  }
40
40
  else {
41
- // eslint-disable-next-line no-console
41
+ // oxlint-disable-next-line no-console
42
42
  console.error(message);
43
43
  }
44
44
  }
@@ -44,7 +44,7 @@ export async function migratePageMetaCustomElementCount(instance, onLog) {
44
44
  onLog(message);
45
45
  }
46
46
  else {
47
- // eslint-disable-next-line no-console
47
+ // oxlint-disable-next-line no-console
48
48
  console.error(message);
49
49
  }
50
50
  }
@@ -46,7 +46,7 @@ export async function migratePageMetaImageScan(instance, onLog) {
46
46
  onLog(message);
47
47
  }
48
48
  else {
49
- // eslint-disable-next-line no-console
49
+ // oxlint-disable-next-line no-console
50
50
  console.error(message);
51
51
  }
52
52
  }
@@ -48,7 +48,7 @@ export async function migratePageTagsToPageTechnologies(instance, onLog) {
48
48
  onLog(startMessage);
49
49
  }
50
50
  else {
51
- // eslint-disable-next-line no-console
51
+ // oxlint-disable-next-line no-console
52
52
  console.error(startMessage);
53
53
  }
54
54
  await instance.transaction(async (trx) => {
@@ -73,7 +73,7 @@ export async function migratePageTagsToPageTechnologies(instance, onLog) {
73
73
  onLog(endMessage);
74
74
  }
75
75
  else {
76
- // eslint-disable-next-line no-console
76
+ // oxlint-disable-next-line no-console
77
77
  console.error(endMessage);
78
78
  }
79
79
  }
@@ -33,6 +33,6 @@ export async function migrateRefTables(instance) {
33
33
  return;
34
34
  }
35
35
  await createRefTables(instance);
36
- // eslint-disable-next-line no-console
36
+ // oxlint-disable-next-line no-console
37
37
  console.error('[migrate] 0.13 ref/header tables created');
38
38
  }
@@ -236,7 +236,7 @@ function resolveOptionalUrlId(value, urlIds, pageId, column) {
236
236
  return null;
237
237
  }
238
238
  if (isLargeDataUri(value)) {
239
- // eslint-disable-next-line no-console
239
+ // oxlint-disable-next-line no-console
240
240
  console.warn(`populatePageMeta: dropping large data URI in page id=${pageId} column=${column} (page_meta has no *_blob_id companion for URL columns)`);
241
241
  return null;
242
242
  }
@@ -102,7 +102,7 @@ export async function populateBlobRefs(trx, onProgress) {
102
102
  }
103
103
  const decoded = decodeDataUri(value);
104
104
  if (decoded === null) {
105
- // eslint-disable-next-line no-console
105
+ // oxlint-disable-next-line no-console
106
106
  console.warn(`populateBlobRefs: skipping malformed data URI (length=${value.length}) — raw string still exists in ${source.table} row`);
107
107
  continue;
108
108
  }
@@ -21,10 +21,10 @@
21
21
  * ```
22
22
  */
23
23
  export async function collectPageIdsWithHtml(archive) {
24
- const rows = (await archive
24
+ const rows = await archive
25
25
  .getKnex()('page_html_ref as ref')
26
26
  .join('page_html_blobs as blob', 'blob.hash', 'ref.hash')
27
27
  .where('blob.size_raw', '>', 0)
28
- .select('ref.page_id as pageId'));
28
+ .select('ref.page_id as pageId');
29
29
  return new Set(rows.map((row) => row.pageId));
30
30
  }
@@ -44,10 +44,10 @@ const SQLITE_IN_CHUNK = 500;
44
44
  */
45
45
  export async function collectPageStylesheetUrls(archive) {
46
46
  const knex = archive.getKnex();
47
- const cssResources = (await knex('resource_items as ri')
47
+ const cssResources = await knex('resource_items as ri')
48
48
  .join('content_type_refs as ctr', 'ctr.id', 'ri.content_type_id')
49
49
  .where('ctr.category', 'css')
50
- .select('ri.id as id', 'ri.url_id as urlId'));
50
+ .select('ri.id as id', 'ri.url_id as urlId');
51
51
  // A CSS resource identified by a `data:` URI (routed to `blob_refs`
52
52
  // instead of `url_refs`, `resource_items.url_blob_id`) has no stylesheet
53
53
  // URL to report — skip it rather than passing a `null` into `whereIn`.
@@ -57,7 +57,9 @@ export async function collectPageStylesheetUrls(archive) {
57
57
  }
58
58
  const cssUrlById = new Map();
59
59
  await eachSplitted(cssResourcesWithUrl.map((r) => r.urlId), SQLITE_IN_CHUNK, async (chunk) => {
60
- const rows = (await knex('url_refs').whereIn('id', chunk).select('id', 'url'));
60
+ const rows = await knex('url_refs')
61
+ .whereIn('id', chunk)
62
+ .select('id', 'url');
61
63
  for (const row of rows) {
62
64
  cssUrlById.set(row.id, row.url);
63
65
  }
@@ -71,9 +73,9 @@ export async function collectPageStylesheetUrls(archive) {
71
73
  }
72
74
  const resourceIdsByPageId = new Map();
73
75
  await eachSplitted(cssResourcesWithUrl.map((r) => r.id), SQLITE_IN_CHUNK, async (chunk) => {
74
- const rows = (await knex('resource_ref_edges')
76
+ const rows = await knex('resource_ref_edges')
75
77
  .whereIn('resource_id', chunk)
76
- .select('resource_id as resourceId', 'page_id as pageId'));
78
+ .select('resource_id as resourceId', 'page_id as pageId');
77
79
  // Avoid `push(...rows)`: on large real archives this chunk array can
78
80
  // be large enough to overflow V8's argument-spread limit even though
79
81
  // the underlying data itself fits in memory.
@@ -92,10 +94,10 @@ export async function collectPageStylesheetUrls(archive) {
92
94
  }
93
95
  const pageUrlById = new Map();
94
96
  await eachSplitted([...resourceIdsByPageId.keys()], SQLITE_IN_CHUNK, async (chunk) => {
95
- const rows = (await knex('content_items as ci')
97
+ const rows = await knex('content_items as ci')
96
98
  .join('url_refs as ur', 'ur.id', 'ci.url_id')
97
99
  .whereIn('ci.id', chunk)
98
- .select('ci.id as id', 'ur.url as url'));
100
+ .select('ci.id as id', 'ur.url as url');
99
101
  for (const row of rows) {
100
102
  pageUrlById.set(row.id, row.url);
101
103
  }
@@ -51,13 +51,13 @@ export async function loadPageClusterCandidates(archive, options) {
51
51
  let lastId = 0;
52
52
  let done = 0;
53
53
  while (true) {
54
- const rows = (await internalHtml()
54
+ const rows = await internalHtml()
55
55
  .join('url_refs as ur', 'ur.id', 'ci.url_id')
56
56
  .leftJoin('page_meta as pm', 'pm.page_id', 'ci.id')
57
57
  .andWhere('ci.id', '>', lastId)
58
58
  .orderBy('ci.id')
59
59
  .limit(batchSize)
60
- .select('ci.id as id', 'ur.url as url', 'pm.main_content_node_name as main_content_node_name', 'pm.main_content_id as main_content_id', 'pm.main_content_role as main_content_role', 'pm.main_content_class_list as main_content_class_list'));
60
+ .select('ci.id as id', 'ur.url as url', 'pm.main_content_node_name as main_content_node_name', 'pm.main_content_id as main_content_id', 'pm.main_content_role as main_content_role', 'pm.main_content_class_list as main_content_class_list');
61
61
  if (rows.length === 0) {
62
62
  break;
63
63
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Orders two strings by UTF-16 code unit, which is what `Array#sort()` does
3
+ * without a comparator. Written out so specs can pass a comparator
4
+ * explicitly (`toSorted(compareStrings)`) and still get the locale-independent
5
+ * order that makes expectations reproducible.
6
+ * @param a - Left string.
7
+ * @param b - Right string.
8
+ * @returns Negative when `a` sorts first, positive when `b` does, 0 when equal.
9
+ * @example
10
+ * ['b', 'B', 'a'].toSorted(compareStrings); // ['B', 'a', 'b']
11
+ */
12
+ export declare function compareStrings(a: string, b: string): number;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Orders two strings by UTF-16 code unit, which is what `Array#sort()` does
3
+ * without a comparator. Written out so specs can pass a comparator
4
+ * explicitly (`toSorted(compareStrings)`) and still get the locale-independent
5
+ * order that makes expectations reproducible.
6
+ * @param a - Left string.
7
+ * @param b - Right string.
8
+ * @returns Negative when `a` sorts first, positive when `b` does, 0 when equal.
9
+ * @example
10
+ * ['b', 'B', 'a'].toSorted(compareStrings); // ['B', 'a', 'b']
11
+ */
12
+ export function compareStrings(a, b) {
13
+ return a < b ? -1 : a > b ? 1 : 0;
14
+ }
@@ -34,7 +34,7 @@ const SRC = TRANSFER_SOURCE_ALIAS;
34
34
  * ```
35
35
  */
36
36
  export async function copyPageTemplateLabels(trx) {
37
- const [sourceTable] = (await trx.raw(`SELECT name FROM ${SRC}.sqlite_master WHERE type = 'table' AND name = 'page_template_labels'`));
37
+ const [sourceTable] = await trx.raw(`SELECT name FROM ${SRC}.sqlite_master WHERE type = 'table' AND name = 'page_template_labels'`);
38
38
  if (sourceTable) {
39
39
  await trx.raw(`
40
40
  INSERT INTO main.page_template_labels (template_key, section, ordinal)
@@ -4,14 +4,14 @@ import type { Knex } from 'knex';
4
4
  * function, in declaration order, minus any names in `exclude`.
5
5
  *
6
6
  * A transfer copy must NEVER build its column list from a hand-written
7
- * literal or from `SELECT *` — migrated archives add columns via `ALTER
8
- * TABLE ADD COLUMN`, which always appends at the end, so a fresh archive's
7
+ * literal or from `SELECT *` — migrated archives add columns via
8
+ * `ALTER TABLE ADD COLUMN`, which always appends at the end, so a fresh archive's
9
9
  * `content_items` (say) can have `alias_of_id` earlier in declaration order
10
- * than a migrated archive's copy of the same table. A positional `INSERT
11
- * ... SELECT * FROM other_table` across two archives with different column
10
+ * than a migrated archive's copy of the same table. A positional
11
+ * `INSERT ... SELECT * FROM other_table` across two archives with different column
12
12
  * orders would silently shift values into the wrong columns. Reading each
13
- * side's own column list and building an explicit, named `SELECT
14
- * <col1>, <col2>, ...` (this function's contract) sidesteps the problem
13
+ * side's own column list and building an explicit, named
14
+ * `SELECT <col1>, <col2>, ...` (this function's contract) sidesteps the problem
15
15
  * entirely — column order never matters when every column is named.
16
16
  *
17
17
  * The pragma table-valued function must be schema-qualified as
@@ -3,14 +3,14 @@
3
3
  * function, in declaration order, minus any names in `exclude`.
4
4
  *
5
5
  * A transfer copy must NEVER build its column list from a hand-written
6
- * literal or from `SELECT *` — migrated archives add columns via `ALTER
7
- * TABLE ADD COLUMN`, which always appends at the end, so a fresh archive's
6
+ * literal or from `SELECT *` — migrated archives add columns via
7
+ * `ALTER TABLE ADD COLUMN`, which always appends at the end, so a fresh archive's
8
8
  * `content_items` (say) can have `alias_of_id` earlier in declaration order
9
- * than a migrated archive's copy of the same table. A positional `INSERT
10
- * ... SELECT * FROM other_table` across two archives with different column
9
+ * than a migrated archive's copy of the same table. A positional
10
+ * `INSERT ... SELECT * FROM other_table` across two archives with different column
11
11
  * orders would silently shift values into the wrong columns. Reading each
12
- * side's own column list and building an explicit, named `SELECT
13
- * <col1>, <col2>, ...` (this function's contract) sidesteps the problem
12
+ * side's own column list and building an explicit, named
13
+ * `SELECT <col1>, <col2>, ...` (this function's contract) sidesteps the problem
14
14
  * entirely — column order never matters when every column is named.
15
15
  *
16
16
  * The pragma table-valued function must be schema-qualified as
@@ -9,8 +9,8 @@ import type { Knex } from 'knex';
9
9
  * target" — a write-time fact this transfer never re-derives, per
10
10
  * ARCHITECTURE.md's invariant), `resource_items.is_external` is a pure
11
11
  * scope check computed the same way at crawl time
12
- * (`@nitpicker/crawler`'s `Crawler#handleResources`: `findScopeEntry(url, this.#scope)
13
- * === null`). Concat's merged scope is a union of every source's roots,
12
+ * (`@nitpicker/crawler`'s `Crawler#handleResources`:
13
+ * `findScopeEntry(url, this.#scope) === null`). Concat's merged scope is a union of every source's roots,
14
14
  * so a resource one source recorded as external can legitimately become
15
15
  * internal-by-scope in the output even though nothing about the resource
16
16
  * itself changed — recomputing this is the resource-table equivalent of
@@ -9,8 +9,8 @@ import { findScopeEntry } from '../scope/find-scope-entry.js';
9
9
  * target" — a write-time fact this transfer never re-derives, per
10
10
  * ARCHITECTURE.md's invariant), `resource_items.is_external` is a pure
11
11
  * scope check computed the same way at crawl time
12
- * (`@nitpicker/crawler`'s `Crawler#handleResources`: `findScopeEntry(url, this.#scope)
13
- * === null`). Concat's merged scope is a union of every source's roots,
12
+ * (`@nitpicker/crawler`'s `Crawler#handleResources`:
13
+ * `findScopeEntry(url, this.#scope) === null`). Concat's merged scope is a union of every source's roots,
14
14
  * so a resource one source recorded as external can legitimately become
15
15
  * internal-by-scope in the output even though nothing about the resource
16
16
  * itself changed — recomputing this is the resource-table equivalent of
@@ -10,5 +10,5 @@ import { splitArray } from '@d-zero/shared/split-array';
10
10
  */
11
11
  export async function eachSplitted(a, count, callback) {
12
12
  const splitted = splitArray(a, count);
13
- await Promise.all(splitted.map((items) => callback(items)));
13
+ await Promise.all(splitted.map((items) => Promise.resolve(callback(items))));
14
14
  }
@@ -166,7 +166,7 @@ export interface MainContentsMainTag {
166
166
  role: string | null;
167
167
  /**
168
168
  * A simple diagnostic CSS selector built from tag + id + classes
169
- * (not `@medv/finder`). // cspell:disable-line
169
+ * (not `@medv/finder`).
170
170
  */
171
171
  selector: string;
172
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitpicker/archive",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "Read/write layer for .nitpicker archives (SQLite schema, migration, cache, concat/split, template classification)",
5
5
  "author": "D-ZERO",
6
6
  "license": "Apache-2.0",
@@ -342,9 +342,9 @@
342
342
  "clean": "tsc --build --clean"
343
343
  },
344
344
  "dependencies": {
345
- "@d-zero/fs": "0.2.7",
346
- "@d-zero/shared": "0.23.0",
347
- "@nitpicker/core": "0.25.0",
345
+ "@d-zero/fs": "0.2.8",
346
+ "@d-zero/shared": "0.23.2",
347
+ "@nitpicker/core": "0.25.1",
348
348
  "debug": "4.4.3",
349
349
  "fs-extra": "11.4.1",
350
350
  "knex": "3.3.0",
@@ -357,5 +357,5 @@
357
357
  "@types/tar": "7.0.87",
358
358
  "@types/unzipper": "0.10.11"
359
359
  },
360
- "gitHead": "fb258acf112366504c6dbb44b69a25bbc528fa30"
360
+ "gitHead": "fa839178ea8f40e92fc742e0480c6cfbbb8fb01f"
361
361
  }