@owrede/vault-memory 2.2.0 → 2.2.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.
package/CHANGELOG.md CHANGED
@@ -14,6 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
14
 
15
15
  _Nothing yet._
16
16
 
17
+ ## [2.2.1] — 2026-06-29
18
+
19
+ ### Fixed
20
+
21
+ - **Section identity is now content + context, not content alone (ADR-032, migration 015).** Two byte-identical sibling sections in *different* contexts (e.g. `# Q1 > ## Risks "TBD"` and `# Q2 > ## Risks "TBD"`) previously collapsed into one row under `UNIQUE(note_id, anchor)` — silently dropping the second — because the content-hash anchor excludes the ancestor chain. Section identity is now `(note_id, heading_path, anchor)`, so differently-placed identical sections persist as distinct rows. `anchor` stays a pure content hash (ADR-003 H-7 and the D-05 `source_hashes` contract are unchanged); `search-sections` dedup widened to match. Migration 015 swaps the unique index; a verbatim block repeated under the *same* parent still collapses (one citation). Existing DBs reconcile on the next `index --full`.
22
+
17
23
  ## [2.2.0] — 2026-06-29
18
24
 
19
25
  ### Added
package/dist/cli.js CHANGED
@@ -1320,7 +1320,7 @@ function backfillSectionsFromChunks(db) {
1320
1320
  @parent_id, @ord, @chunk_id_first, @chunk_id_last, @created_at)
1321
1321
  `);
1322
1322
  const lookupExistingSection = db.prepare(
1323
- "SELECT id FROM sections WHERE note_id = ? AND anchor = ?"
1323
+ "SELECT id FROM sections WHERE note_id = ? AND heading_path = ? AND anchor = ?"
1324
1324
  );
1325
1325
  let backfilled = 0;
1326
1326
  const now = Date.now();
@@ -1356,7 +1356,11 @@ function backfillSectionsFromChunks(db) {
1356
1356
  if (info.changes > 0) {
1357
1357
  insertedIds.push(Number(info.lastInsertRowid));
1358
1358
  } else {
1359
- const existing2 = lookupExistingSection.get(note.id, s.anchor);
1359
+ const existing2 = lookupExistingSection.get(
1360
+ note.id,
1361
+ JSON.stringify(s.heading_path),
1362
+ s.anchor
1363
+ );
1360
1364
  insertedIds.push(existing2 ? Number(existing2.id) : null);
1361
1365
  }
1362
1366
  }
@@ -1688,6 +1692,11 @@ function runMigration014(db, _ctx) {
1688
1692
  ON contract_audit(verb);
1689
1693
  `);
1690
1694
  }
1695
+ function runMigration015(db, _ctx) {
1696
+ db.exec(
1697
+ "DROP INDEX IF EXISTS sections_note_anchor; CREATE UNIQUE INDEX IF NOT EXISTS sections_note_headingpath_anchor ON sections(note_id, heading_path, anchor);"
1698
+ );
1699
+ }
1691
1700
  var INITIAL_SCHEMA, MIGRATION_002_ALIASES, MIGRATION_003_FIX_DELETE_FKS, MIGRATION_004_VARIABLE_DIMS, MIGRATION_006_BODY_HASH, MIGRATION_007_DOC_URI_ADD, MIGRATIONS;
1692
1701
  var init_schema = __esm({
1693
1702
  "src/db/schema.ts"() {
@@ -1963,6 +1972,11 @@ CREATE INDEX IF NOT EXISTS idx_notes_doc_uri ON notes(doc_uri);
1963
1972
  version: 14,
1964
1973
  description: "contract_audit table \u2014 Phase 6 / CON-* / Q-AUD",
1965
1974
  run: runMigration014
1975
+ },
1976
+ {
1977
+ version: 15,
1978
+ description: "section identity = (note_id, heading_path, anchor) \u2014 context-aware, no longer collapse byte-identical siblings in different contexts (ADR-032 revised)",
1979
+ run: runMigration015
1966
1980
  }
1967
1981
  ];
1968
1982
  }
@@ -3067,6 +3081,9 @@ var init_sections = __esm({
3067
3081
  this._getByAnchor = db.prepare(
3068
3082
  "SELECT * FROM sections WHERE note_id = ? AND anchor = ?"
3069
3083
  );
3084
+ this._getByIdentity = db.prepare(
3085
+ "SELECT * FROM sections WHERE note_id = ? AND heading_path = ? AND anchor = ?"
3086
+ );
3070
3087
  this._findContainingChunk = db.prepare(
3071
3088
  // `chunk_id` is monotonically increasing per note; chunk_id_first
3072
3089
  // and chunk_id_last carve disjoint ranges (or both NULL for a
@@ -3090,6 +3107,7 @@ var init_sections = __esm({
3090
3107
  _deleteByNote;
3091
3108
  _getByNote;
3092
3109
  _getByAnchor;
3110
+ _getByIdentity;
3093
3111
  _findContainingChunk;
3094
3112
  _countByNote;
3095
3113
  /**
@@ -3122,14 +3140,15 @@ var init_sections = __esm({
3122
3140
  return ids;
3123
3141
  }
3124
3142
  /**
3125
- * Insert one section, collision-safe. Returns the id of the row that
3126
- * now owns (note_id, anchor): the freshly inserted row, or — when a
3127
- * same-anchor sibling already won the unique slot — that surviving
3128
- * row's id (so callers can resolve parent_id linkage). Mirrors the
3129
- * backfill behavior in src/sections/backfill.ts. The live indexer
3130
- * uses this instead of `insertMany` so duplicate-anchor sibling
3131
- * headings can't abort the whole index run
3132
- * (see ISSUE-indexer-duplicate-anchor.md).
3143
+ * Insert one section, collision-safe. Returns the id of the row that now
3144
+ * owns the identity (note_id, heading_path, anchor): the freshly inserted
3145
+ * row, or — when a same-context byte-identical sibling already won the
3146
+ * unique slot — that surviving row's id (so callers can resolve parent_id
3147
+ * linkage). Per ADR-032 (revised), a collision now requires BOTH same anchor
3148
+ * AND same heading_path, so differently-placed identical sections persist as
3149
+ * distinct rows. Mirrors src/sections/backfill.ts. The live indexer uses
3150
+ * this instead of `insertMany` so duplicate sibling headings can't abort the
3151
+ * whole index run (see ISSUE-indexer-duplicate-anchor.md).
3133
3152
  */
3134
3153
  insertOneResolving(r) {
3135
3154
  const info = this._insert.run({
@@ -3145,7 +3164,7 @@ var init_sections = __esm({
3145
3164
  created_at: Date.now()
3146
3165
  });
3147
3166
  if (info.changes > 0) return Number(info.lastInsertRowid);
3148
- const existing = this._getByAnchor.get(r.note_id, r.anchor);
3167
+ const existing = this._getByIdentity.get(r.note_id, r.heading_path, r.anchor);
3149
3168
  return existing ? Number(existing.id) : null;
3150
3169
  }
3151
3170
  deleteByNote(noteId) {
@@ -10756,7 +10775,7 @@ async function searchSections(deps, args2) {
10756
10775
  const resolution = deps.sectionForHit(hit.vault, hit.notePath, hit.chunkIdx);
10757
10776
  if (!resolution) continue;
10758
10777
  if (resolution.headingPath.length === 0) continue;
10759
- const key = `${resolution.noteId}#${resolution.anchor}`;
10778
+ const key = `${resolution.noteId}#${resolution.headingPath.join("\0")}#${resolution.anchor}`;
10760
10779
  const existing = sectionMap.get(key);
10761
10780
  if (!existing) {
10762
10781
  sectionMap.set(key, {