@moxn/kb-migrate 0.4.38 → 0.4.39

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.
@@ -150,7 +150,7 @@ export declare function slugify(title: string): string;
150
150
  * so a second same-titled sibling was rejected by that assert (audit finding D).
151
151
  * Mutates `siblingSlugCounts` (keyed on the BASE slug) the way the builder expects.
152
152
  */
153
- export declare function disambiguateSibling(title: string, siblingSlugCounts: Map<string, number>): {
153
+ export declare function disambiguateSibling(title: string, siblingSlugCounts: Map<string, number>, slugifyFn?: (s: string) => string): {
154
154
  name: string;
155
155
  slug: string;
156
156
  };
@@ -632,12 +632,12 @@ export function slugify(title) {
632
632
  * so a second same-titled sibling was rejected by that assert (audit finding D).
633
633
  * Mutates `siblingSlugCounts` (keyed on the BASE slug) the way the builder expects.
634
634
  */
635
- export function disambiguateSibling(title, siblingSlugCounts) {
636
- const baseSlug = slugify(title);
635
+ export function disambiguateSibling(title, siblingSlugCounts, slugifyFn = slugify) {
636
+ const baseSlug = slugifyFn(title);
637
637
  const existing = siblingSlugCounts.get(baseSlug) ?? 0;
638
638
  siblingSlugCounts.set(baseSlug, existing + 1);
639
639
  if (existing === 0)
640
640
  return { name: title, slug: baseSlug };
641
641
  const name = `${title} ${existing + 1}`;
642
- return { name, slug: slugify(name) };
642
+ return { name, slug: slugifyFn(name) };
643
643
  }
@@ -68,3 +68,16 @@ describe('disambiguateSibling (D — dedup name AND path together)', () => {
68
68
  expect(disambiguateSibling('Beta', counts).slug).toBe('beta');
69
69
  });
70
70
  });
71
+ describe('disambiguateSibling — custom slugify (OneNote uses slugifyPathSegment)', () => {
72
+ const upperSlug = (s) => s.toUpperCase().replace(/ /g, '_');
73
+ it('derives the slug with the provided function, keeping the bijection', () => {
74
+ const counts = new Map();
75
+ expect(disambiguateSibling('My Page', counts, upperSlug)).toEqual({
76
+ name: 'My Page',
77
+ slug: 'MY_PAGE',
78
+ });
79
+ const second = disambiguateSibling('My Page', counts, upperSlug);
80
+ expect(second).toEqual({ name: 'My Page 2', slug: 'MY_PAGE_2' });
81
+ expect(upperSlug(second.name)).toBe(second.slug);
82
+ });
83
+ });
@@ -11,6 +11,7 @@
11
11
  * own ID or any ancestor's ID is in the set. If the set is empty, every
12
12
  * page is included.
13
13
  */
14
+ import { disambiguateSibling } from '../notion.js';
14
15
  const MAX_GROUP_DEPTH = 4;
15
16
  /**
16
17
  * Normalize any human text into a ltree-safe slug segment.
@@ -96,12 +97,22 @@ async function walkSection(client, section, pathSegs, ctx, ancestorSelected, sel
96
97
  createdBefore: opts.createdBefore,
97
98
  });
98
99
  const sectionSeg = slugifyPathSegment(section.displayName);
100
+ // Dedup same-titled sibling pages within a section — suffix the title AND the
101
+ // path segment together so slugify(name) === pathTail (the server's hard
102
+ // bijection assert) and two pages never collide on one path. Without this,
103
+ // two "Untitled" pages produce the same kbPath → the second silently clobbers
104
+ // or skips the first on import. (Notion finding D; OneNote had no dedup at all.)
105
+ const pageSlugCounts = new Map();
99
106
  for (const page of pages) {
100
107
  const pageSelected = ancestorSelected || (!!sel && sel.has(page.id));
101
108
  if (!pageSelected)
102
109
  continue;
103
- const pageSeg = slugifyPathSegment(page.title ?? 'untitled');
104
- out.push(toDiscovered(page, section, [...pathSegs, sectionSeg, pageSeg], ctx));
110
+ const { name: dedupTitle, slug: pageSeg } = disambiguateSibling(page.title ?? 'untitled', pageSlugCounts, slugifyPathSegment);
111
+ out.push(toDiscovered({ ...page, title: dedupTitle }, section, [
112
+ ...pathSegs,
113
+ sectionSeg,
114
+ pageSeg,
115
+ ], ctx));
105
116
  }
106
117
  }
107
118
  function toDiscovered(page, section, segs, ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxn/kb-migrate",
3
- "version": "0.4.38",
3
+ "version": "0.4.39",
4
4
  "description": "Migration tool for importing documents into Moxn Knowledge Base from local files, Notion, Google Docs, and more",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",