watashi-db 0.0.21 → 0.0.22

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
  {
2
2
  "name": "watashi-db",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "わたしDB - パーソナルコンテキストデータベース。ユーザーの好み・スキル・価値観・意思決定をセッション横断で記憶・活用します。",
5
5
  "author": { "name": "watashi-db" },
6
6
  "repository": "https://github.com/bareforge/watashi-db",
package/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.22] - 2026-06-10
11
+
12
+ ### Added
13
+ - **マージのレコード単位耐性(柱1)** — `mergeFromRemote` の各テーブルマージを 1 レコード単位の try-catch にし、制約違反等で 1 件が失敗しても残りのレコード・他テーブルの取り込みを続行する。`MergeStats` に `failed` / `errors`(代表最大 10 件)を追加。従来は 1 件の NOT NULL 違反でマージ全体が中断し、それが stderr にしか出ないため同期停止がサイレントに進行していた。
14
+ - **sync 健康の hook 通知(柱2)** — 同期マージの失敗を store_meta(`last_merge_error` / `last_merge_error_at`)に永続化し、UserPromptSubmit hook が毎プロンプト軽量に読んで「【⚠ watashi-db 同期の異常】」として注入する。通知対象: (1) 直近マージで 1 件も取り込めなかった理由(スキーマバージョン不一致スキップ含む — 従来 stderr のみのサイレント経路)、(2) レコード単位の取り込み失敗の要約、(3) 他デバイスからの取り込みが 7 日以上成功していない(`last_synced_at` ベース)、(4) schema drift 検出結果(`schema_drift_summary`)。
15
+ - **schema drift 検出(柱5)** — `watashi_maintain action='schema_drift'` を新設。正規スキーマ(最新 `initializeSchema` を適用した :memory: DB)と実 DB の PRAGMA を突き合わせ、同じ schema_version でも生じる差異(NOT NULL 制約の欠落・カラム欠落・型不一致)を検出する。サーバー起動時にも store ごとに 1 回検出して `schema_drift_summary` を store_meta に記録し、hook 通知に乗せる。
16
+
17
+ ### Fixed
18
+ - **claims の NOT NULL 制約 drift による sync サイレント破綻** — `ALTER TABLE ... RENAME COLUMN` が旧カラムの制約を引き継ぐため、マイグレーション経由の既存 DB と新規 DB(フルスキーマ定義)で `claims.l2_subject` 等の NOT NULL 有無がズレ、デバイス間マージが NOT NULL 違反でサイレントに失敗し続けていた(実環境で 83 日間同期停止)。V37 で全 DB を「NOT NULL なし」へ収束(緩和方向=既存データ無損失。SPO 必須はアプリ層 `store_claim` で担保)。フルスキーマ(applyV1 squash)側からも NOT NULL を除去し、新規 DB との drift 再発を防止。
19
+
20
+ ### Migration
21
+ - **V37 への自動マイグレーション** — claims テーブルを再作成して `l2_subject` / `l2_predicate` / `l2_object` の NOT NULL を除去(`l1_content` の逆向き drift も解消)。退避したトリガー/インデックスの復元は、旧バージョンの残骸(V32 が DROP できなかった旧 `status` カラムを参照する `idx_claims_status` 等)で失敗しうるため個別 try-catch でスキップ+stderr 警告(残骸はテーブル再作成とともに消滅するのが正しい)。unified_search 連携トリガー(V32 形式・全 11 テーブル)と claims_fts 同期トリガー(V28 形式)は正規形で再作成し、V31 形式のまま残存していた旧トリガーも収束させる。冪等チェックは「`claims_unified_insert` が旧 status を参照していたら再作成」を含む。ロールバック不要。
22
+ - **注意: 同期は全デバイスが V37 に揃うまで再開されない** — マージは同一 schema_version のみ対象のため、本バージョンへ全デバイスを更新すること(不一致は hook 通知で可視化される)。
23
+
24
+ ### Tests
25
+ - `tests/schema.test.ts` に V37 検証を追加(NOT NULL 除去・status 残骸掃除・トリガー正規形・claims_fts 動作・新規 DB の制約収束)。全 968 テスト緑。
26
+
10
27
  ## [0.0.21] - 2026-06-08
11
28
 
12
29
  ### Added
package/LICENSE CHANGED
@@ -3,7 +3,7 @@ Business Source License 1.1
3
3
  Parameters
4
4
 
5
5
  Licensor: bareforge
6
- Licensed Work: watashi-db 0.0.21
6
+ Licensed Work: watashi-db 0.0.22
7
7
  The Licensed Work is (c) 2026 bareforge
8
8
  Additional Use Grant: You may make use of the Licensed Work, provided that
9
9
  you may not use the Licensed Work for a Commercial
@@ -14,7 +14,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
14
14
  or other commercial consideration.
15
15
  Non-commercial use, personal use, educational use,
16
16
  and evaluation use are always permitted.
17
- Change Date: 2029-06-08
17
+ Change Date: 2029-06-10
18
18
  Change License: Apache License, Version 2.0
19
19
 
20
20
  For information about alternative licensing arrangements for the Licensed Work,
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/database/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAgD3C,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAEjD;AAOD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAwF5D"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/database/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAiD3C,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAEjD;AAOD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAwF5D"}
@@ -38,7 +38,8 @@ import fs from "node:fs";
38
38
  // 2026-03-24 修正: V31追加(File Vault — user_files テーブル + unified search統合)
39
39
  // 2026-04-02 修正: V34追加(user_topics.device_id 欠落修復)
40
40
  // 2026-05-29 修正: V36追加(practices テーブル新設 — 作業実践の判断則セット、Decision 01KSPW75M429QKY5FZVJ6P5N9H)
41
- const SCHEMA_VERSION = 36;
41
+ // 2026-06-10 修正: V37追加(claims SPO/l1_content の NOT NULL 制約を全DBで「なし」に収束 — sync サイレント破綻対策)
42
+ const SCHEMA_VERSION = 37;
42
43
  // devMode フラグ(startServer から schema 初期化経路に伝播)
43
44
  // develop.db では released: false のマイグレーションも適用する
44
45
  let _devMode = false;
@@ -251,6 +252,9 @@ function _applyMigrations(db, version) {
251
252
  if (version < 36) {
252
253
  applyV36(db);
253
254
  }
255
+ if (version < 37) {
256
+ applyV37(db);
257
+ }
254
258
  }
255
259
  /**
256
260
  * V1スキーマ: V30相当のフルスキーマ(squash済み)
@@ -492,12 +496,14 @@ function applyV1(db) {
492
496
  db.exec(`
493
497
  -- --------------------------------------------------------
494
498
  -- claims: 知識断片(L2 SPO三つ組)
499
+ -- V37: SPO に NOT NULL を付けない(旧DBとの制約収束。SPO 必須はアプリ層 store_claim で担保)。
500
+ -- ここに NOT NULL を戻すと新規DBが毎回 applyV37 の再作成パスを通り、マージ非互換も再発する。
495
501
  -- --------------------------------------------------------
496
502
  CREATE TABLE IF NOT EXISTS claims (
497
503
  id TEXT PRIMARY KEY,
498
- l2_subject TEXT NOT NULL,
499
- l2_predicate TEXT NOT NULL,
500
- l2_object TEXT NOT NULL,
504
+ l2_subject TEXT,
505
+ l2_predicate TEXT,
506
+ l2_object TEXT,
501
507
  category TEXT NOT NULL CHECK(category IN ('preference','identity','skill','value','workflow','knowledge','custom')),
502
508
  scope TEXT NOT NULL DEFAULT 'global',
503
509
  confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
@@ -7590,6 +7596,151 @@ function hasColumn(db, table, column) {
7590
7596
  *
7591
7597
  * Decision: 01KSPW75M429QKY5FZVJ6P5N9H
7592
7598
  */
7599
+ /**
7600
+ * V37: claims の l2_subject/l2_predicate/l2_object の NOT NULL 制約を外す(緩和方向の制約収束)。
7601
+ *
7602
+ * 背景: applyV27 の `ALTER TABLE claims RENAME COLUMN subject TO l2_subject` は旧カラムの制約を引き継ぐため、
7603
+ * 旧 subject が NOT NULL でないDBでは l2_subject も NOT NULL を持たず、新規DB(フルスキーマ定義は NOT NULL)と
7604
+ * 制約がズレた。この差が device 間マージで NOT NULL 違反を起こし sync をサイレント破綻させていた。
7605
+ * 全DBを「NOT NULL なし」へ収束させマージ非互換を解消する(制約を緩める方向=既存データは無損失)。
7606
+ * SPO 必須はアプリ層(store_claim)で担保する。l1_content の逆向き drift(旧DBで NOT NULL 残存)も同時に解消する。
7607
+ *
7608
+ * SQLite は列制約の変更にテーブル再作成が要る。claims に依存するトリガー/インデックスは
7609
+ * sqlite_master から定義を取得して動的復元する(定義のハードコード漏れと FTS 破損を防ぐ)。
7610
+ */
7611
+ function applyV37(db) {
7612
+ // 冪等性: l2_subject と l1_content がどちらも正規(NOT NULLなし)で、unified トリガーも
7613
+ // 正規形(V31 形式の new.status 参照が残っていない)なら再作成不要
7614
+ const cols = db.prepare("PRAGMA table_info(claims)").all();
7615
+ const subj = cols.find((c) => c.name === "l2_subject");
7616
+ const l1 = cols.find((c) => c.name === "l1_content");
7617
+ const trgSql = db.prepare("SELECT sql FROM sqlite_master WHERE type='trigger' AND name='claims_unified_insert'").get()?.sql ?? "";
7618
+ const staleTrigger = /new\.status\b/.test(trgSql);
7619
+ if (subj && subj.notnull === 0 && l1 && l1.notnull === 0 && !staleTrigger) {
7620
+ db.exec("INSERT INTO schema_version (version) VALUES (37)");
7621
+ return;
7622
+ }
7623
+ // claim_history/claim_evidence 等が claims(id) を参照するため、再作成中は FK を一時無効化する
7624
+ // (applyV7 等の既存テーブル再作成と同じパターン。PRAGMA は transaction 外で設定する必要がある)
7625
+ const fkState = db.pragma("foreign_keys");
7626
+ db.pragma("foreign_keys = OFF");
7627
+ const transaction = db.transaction(() => {
7628
+ // claims に付随するトリガー/インデックスの定義を退避(DROP TABLE で連鎖削除されるため)
7629
+ const aux = db.prepare("SELECT sql FROM sqlite_master WHERE tbl_name='claims' AND type IN ('trigger','index') AND sql IS NOT NULL").all();
7630
+ // NOT NULL を l2_subject/predicate/object から外した新テーブル(他カラムは applyV1 と同一)
7631
+ db.exec(`
7632
+ CREATE TABLE claims_new (
7633
+ id TEXT PRIMARY KEY,
7634
+ l2_subject TEXT,
7635
+ l2_predicate TEXT,
7636
+ l2_object TEXT,
7637
+ category TEXT NOT NULL CHECK(category IN ('preference','identity','skill','value','workflow','knowledge','custom')),
7638
+ scope TEXT NOT NULL DEFAULT 'global',
7639
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
7640
+ l2_evidence TEXT,
7641
+ l2_falsifier TEXT,
7642
+ l1_content TEXT,
7643
+ search_summary TEXT,
7644
+ l1_embedding BLOB,
7645
+ hit_count INTEGER NOT NULL DEFAULT 0,
7646
+ last_hit_at TEXT,
7647
+ promoted_from_store TEXT,
7648
+ promoted_from_id TEXT,
7649
+ validity_status TEXT NOT NULL DEFAULT 'active' CHECK(validity_status IN ('active','invalidated','superseded')),
7650
+ is_archived INTEGER NOT NULL DEFAULT 0,
7651
+ promoted_to TEXT,
7652
+ user_input TEXT,
7653
+ source_tool TEXT,
7654
+ session_id TEXT,
7655
+ client_name TEXT,
7656
+ client_version TEXT,
7657
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
7658
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
7659
+ )
7660
+ `);
7661
+ // データ移行(明示カラム指定で順序非依存)
7662
+ const colNames = [
7663
+ "id", "l2_subject", "l2_predicate", "l2_object", "category", "scope", "confidence",
7664
+ "l2_evidence", "l2_falsifier", "l1_content", "search_summary", "l1_embedding",
7665
+ "hit_count", "last_hit_at", "promoted_from_store", "promoted_from_id", "validity_status",
7666
+ "is_archived", "promoted_to", "user_input", "source_tool", "session_id",
7667
+ "client_name", "client_version", "created_at", "updated_at",
7668
+ ].join(", ");
7669
+ db.exec(`INSERT INTO claims_new (${colNames}) SELECT ${colNames} FROM claims`);
7670
+ db.exec("DROP TABLE claims");
7671
+ db.exec("ALTER TABLE claims_new RENAME TO claims");
7672
+ // 退避したトリガー/インデックスを復元(claims_fts/unified_search への外部コンテンツトリガー含む)。
7673
+ // 旧マイグレーション経路の DB には claims_new に存在しないカラムを参照する残骸定義が残りうる
7674
+ // (例: V32 が DROP できなかった旧 status カラムへの idx_claims_status、V31 形式の unified トリガー)。
7675
+ // 残骸はテーブル再作成とともに消滅するのが正しいため、復元失敗はスキップして警告のみ出す。
7676
+ for (const { sql } of aux) {
7677
+ try {
7678
+ db.exec(sql);
7679
+ }
7680
+ catch (e) {
7681
+ process.stderr.write(`[watashi-db] V37: 残骸定義の復元をスキップ: ${sql.replace(/\s+/g, " ").slice(0, 100)} ` +
7682
+ `(${e instanceof Error ? e.message : String(e)})\n`);
7683
+ }
7684
+ }
7685
+ // unified_search トリガーを正規形で再作成する(V32 と同じパターン)。
7686
+ // 実DBには V32 のトリガー再作成が効いていない V31 形式(new.status 参照)の unified トリガーが
7687
+ // 残存しうる(claims 分は上の復元で消えるが、他テーブル分は放置すると検索インデックスの劣化が続く)。
7688
+ const allTriggerTables = ["claims", "episodes", "decisions", "theories", "insights", "models",
7689
+ "user_memos", "user_plans", "user_issues", "user_topics", "user_files"];
7690
+ for (const tbl of allTriggerTables) {
7691
+ db.exec(`DROP TRIGGER IF EXISTS ${tbl}_unified_insert`);
7692
+ db.exec(`DROP TRIGGER IF EXISTS ${tbl}_unified_update`);
7693
+ db.exec(`DROP TRIGGER IF EXISTS ${tbl}_unified_delete`);
7694
+ }
7695
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_insert`);
7696
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_update`);
7697
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_delete`);
7698
+ createUnifiedSearchTriggers(db);
7699
+ // claims_fts 同期トリガーも正規形(V28 形式)で再作成する(復元で旧定義が立った場合の収束)
7700
+ db.exec(`DROP TRIGGER IF EXISTS claims_fts_insert`);
7701
+ db.exec(`DROP TRIGGER IF EXISTS claims_fts_update`);
7702
+ db.exec(`DROP TRIGGER IF EXISTS claims_fts_delete`);
7703
+ db.exec(`
7704
+ CREATE TRIGGER claims_fts_insert AFTER INSERT ON claims BEGIN
7705
+ INSERT INTO claims_fts(rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
7706
+ VALUES (new.rowid, new.l2_subject, new.l2_predicate, new.l2_object, new.l2_evidence, new.l1_content, new.search_summary);
7707
+ END
7708
+ `);
7709
+ db.exec(`
7710
+ CREATE TRIGGER claims_fts_update AFTER UPDATE ON claims BEGIN
7711
+ INSERT INTO claims_fts(claims_fts, rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
7712
+ VALUES ('delete', old.rowid, old.l2_subject, old.l2_predicate, old.l2_object, old.l2_evidence, old.l1_content, old.search_summary);
7713
+ INSERT INTO claims_fts(rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
7714
+ VALUES (new.rowid, new.l2_subject, new.l2_predicate, new.l2_object, new.l2_evidence, new.l1_content, new.search_summary);
7715
+ END
7716
+ `);
7717
+ db.exec(`
7718
+ CREATE TRIGGER claims_fts_delete AFTER DELETE ON claims BEGIN
7719
+ INSERT INTO claims_fts(claims_fts, rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
7720
+ VALUES ('delete', old.rowid, old.l2_subject, old.l2_predicate, old.l2_object, old.l2_evidence, old.l1_content, old.search_summary);
7721
+ END
7722
+ `);
7723
+ // claims の正規インデックスを補完(残骸 idx_claims_status は復元スキップで消滅済み。IF NOT EXISTS で冪等)
7724
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_claims_category ON claims(category)`);
7725
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_claims_scope ON claims(scope)`);
7726
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_claims_validity_status ON claims(validity_status)`);
7727
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_claims_is_archived ON claims(is_archived)`);
7728
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_claims_l2_subject ON claims(l2_subject)`);
7729
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_claims_updated ON claims(updated_at)`);
7730
+ db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_claims_promoted_from ON claims(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL`);
7731
+ // claims_fts(外部コンテンツ FTS)を再構築
7732
+ try {
7733
+ db.exec("INSERT INTO claims_fts(claims_fts) VALUES('rebuild')");
7734
+ }
7735
+ catch { /* claims_fts が無い構成では無視 */ }
7736
+ db.exec("INSERT INTO schema_version (version) VALUES (37)");
7737
+ });
7738
+ transaction();
7739
+ // foreign_keys を元の状態に復元
7740
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
7741
+ db.pragma("foreign_keys = ON");
7742
+ }
7743
+ }
7593
7744
  function applyV36(db) {
7594
7745
  const transaction = db.transaction(() => {
7595
7746
  // === 1. practices テーブル作成 ===