@moxn/kb-migrate 0.4.39 → 0.4.41

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/dist/client.d.ts CHANGED
@@ -27,6 +27,11 @@ export interface ImportMarkdownResult {
27
27
  name?: string;
28
28
  /** True when a front-matter block was present but NOT applied (no name → body-only import). */
29
29
  frontMatterIgnored?: boolean;
30
+ /**
31
+ * Set when strict YAML parsing of the front-matter block failed, even if the
32
+ * lenient fallback recovered the name. Carries the js-yaml error message.
33
+ */
34
+ frontMatterError?: string;
30
35
  }
31
36
  export declare class MoxnClient {
32
37
  private apiUrl;
@@ -100,13 +100,16 @@ export async function runLocalGrammarMigration(source, options) {
100
100
  const client = new MoxnClient(options);
101
101
  let processed = 0;
102
102
  let consecutiveFailures = 0;
103
+ let fmIgnoredOrDegraded = 0;
103
104
  const MAX_CONSECUTIVE_FAILURES = 10;
104
105
  for await (const file of source.extractGrammarFiles()) {
105
106
  processed++;
106
107
  const progress = totalCount ? ` (${processed}/${totalCount})` : '';
107
108
  console.log(`Processing: ${file.sourcePath}${progress}`);
108
- const result = await migrateGrammarFile(client, file, options);
109
+ const { result, hadFmWarning } = await migrateGrammarFile(client, file, options);
109
110
  results.push(result);
111
+ if (hadFmWarning)
112
+ fmIgnoredOrDegraded++;
110
113
  if (result.status === 'failed') {
111
114
  consecutiveFailures++;
112
115
  }
@@ -138,6 +141,7 @@ export async function runLocalGrammarMigration(source, options) {
138
141
  skipped: results.filter((r) => r.status === 'skipped').length,
139
142
  failed: results.filter((r) => r.status === 'failed').length,
140
143
  duration: Date.now() - startTime,
144
+ ...(fmIgnoredOrDegraded > 0 ? { fmIgnoredOrDegraded } : {}),
141
145
  };
142
146
  return {
143
147
  timestamp: new Date().toISOString(),
@@ -155,10 +159,13 @@ async function migrateGrammarFile(client, file, options) {
155
159
  const documentPath = joinKbPath(options.basePath, file.kbPath);
156
160
  if (options.dryRun) {
157
161
  return {
158
- sourcePath: file.sourcePath,
159
- documentPath,
160
- status: 'skipped',
161
- duration: Date.now() - startTime,
162
+ result: {
163
+ sourcePath: file.sourcePath,
164
+ documentPath,
165
+ status: 'skipped',
166
+ duration: Date.now() - startTime,
167
+ },
168
+ hadFmWarning: false,
162
169
  };
163
170
  }
164
171
  try {
@@ -175,11 +182,18 @@ async function migrateGrammarFile(client, file, options) {
175
182
  onConflict: options.onConflict,
176
183
  });
177
184
  // Surface a non-fatal warning when the file carried a front-matter block
178
- // that wasn't applied (no `name` → body-only import): tags/description/etc.
179
- // declared in it were ignored.
185
+ // that wasn't applied (no `name` → body-only import), OR when strict YAML
186
+ // parsing failed (even if the lenient fallback recovered the name, some
187
+ // metadata like tags/properties may have been lost).
188
+ let hadFmWarning = false;
180
189
  if (res.frontMatterIgnored) {
190
+ hadFmWarning = true;
181
191
  console.warn(` ⚠ front-matter present but not applied (no \`name\`) — imported body only: ${file.sourcePath}`);
182
192
  }
193
+ else if (res.frontMatterError) {
194
+ hadFmWarning = true;
195
+ console.warn(` ⚠ front-matter YAML parse error (tags/properties may be missing): ${res.frontMatterError} — ${file.sourcePath}`);
196
+ }
183
197
  // Map import_markdown outcome → migration status.
184
198
  const status = res.outcome === 'created'
185
199
  ? 'created'
@@ -187,20 +201,26 @@ async function migrateGrammarFile(client, file, options) {
187
201
  ? 'updated'
188
202
  : 'skipped';
189
203
  return {
190
- sourcePath: file.sourcePath,
191
- documentPath: res.path || documentPath,
192
- status,
193
- documentId: res.id,
194
- duration: Date.now() - startTime,
204
+ result: {
205
+ sourcePath: file.sourcePath,
206
+ documentPath: res.path || documentPath,
207
+ status,
208
+ documentId: res.id,
209
+ duration: Date.now() - startTime,
210
+ },
211
+ hadFmWarning,
195
212
  };
196
213
  }
197
214
  catch (error) {
198
215
  return {
199
- sourcePath: file.sourcePath,
200
- documentPath,
201
- status: 'failed',
202
- error: error instanceof Error ? error.message : 'Unknown error',
203
- duration: Date.now() - startTime,
216
+ result: {
217
+ sourcePath: file.sourcePath,
218
+ documentPath,
219
+ status: 'failed',
220
+ error: error instanceof Error ? error.message : 'Unknown error',
221
+ duration: Date.now() - startTime,
222
+ },
223
+ hadFmWarning: false,
204
224
  };
205
225
  }
206
226
  }
@@ -1,5 +1,9 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { joinKbPath } from './import-local.js';
1
+ import { describe, it, expect, vi, afterEach, beforeEach, } from 'vitest';
2
+ import { joinKbPath, runLocalGrammarMigration } from './import-local.js';
3
+ // Mock fs/promises at module level so readFile is consistently stubable.
4
+ vi.mock('fs/promises', () => ({
5
+ readFile: vi.fn().mockResolvedValue('---\nname: test-doc\n---\n\n## Body\n\ncontent'),
6
+ }));
3
7
  describe('joinKbPath', () => {
4
8
  it('prefixes the relative KB path under a base path', () => {
5
9
  expect(joinKbPath('/imported', 'subdir/doc')).toBe('/imported/subdir/doc');
@@ -17,3 +21,99 @@ describe('joinKbPath', () => {
17
21
  expect(joinKbPath('base', 'doc')).toBe('/base/doc');
18
22
  });
19
23
  });
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+ // FM warning + summary count tests
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+ const BASE_OPTIONS = {
28
+ apiUrl: 'http://test-api',
29
+ apiKey: 'test-key',
30
+ basePath: '/imported',
31
+ onConflict: 'update',
32
+ dryRun: false,
33
+ };
34
+ function mockResponse(status, body) {
35
+ return {
36
+ ok: status >= 200 && status < 300,
37
+ status,
38
+ json: async () => body,
39
+ };
40
+ }
41
+ function fakeSource(kbPath = 'my-doc.md') {
42
+ return {
43
+ sourceType: 'local',
44
+ sourceLocation: '/fake',
45
+ validate: async () => { },
46
+ getDocumentCount: async () => 1,
47
+ async *extractGrammarFiles() {
48
+ yield {
49
+ fullPath: '/fake/my-doc.md',
50
+ sourcePath: 'my-doc.md',
51
+ kbPath,
52
+ };
53
+ },
54
+ };
55
+ }
56
+ describe('runLocalGrammarMigration — FM warning surface + summary count', () => {
57
+ let warnSpy;
58
+ let logSpy;
59
+ let fetchSpy;
60
+ beforeEach(() => {
61
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
62
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
63
+ fetchSpy = vi.spyOn(globalThis, 'fetch');
64
+ });
65
+ afterEach(() => {
66
+ vi.restoreAllMocks();
67
+ });
68
+ it('includes frontMatterError text in the console warning when server returns it', async () => {
69
+ fetchSpy.mockResolvedValue(mockResponse(200, {
70
+ result: {
71
+ id: 'doc-1',
72
+ path: '/imported/test',
73
+ outcome: 'created',
74
+ existed: false,
75
+ name: 'test',
76
+ frontMatterIgnored: false,
77
+ frontMatterError: 'strict YAML parse failed (bad indentation of a mapping entry); recovered name/description via lenient fallback',
78
+ },
79
+ }));
80
+ const log = await runLocalGrammarMigration(fakeSource(), BASE_OPTIONS);
81
+ // Warning must be emitted and include the frontMatterError message
82
+ expect(warnSpy).toHaveBeenCalledOnce();
83
+ const warnMsg = String(warnSpy.mock.calls[0][0]);
84
+ expect(warnMsg).toMatch(/YAML parse error|frontMatterError|bad indentation/i);
85
+ // Summary must carry the count
86
+ expect(log.summary.fmIgnoredOrDegraded).toBe(1);
87
+ void logSpy; // suppress unused warning
88
+ });
89
+ it('includes frontMatterIgnored warning when server returns frontMatterIgnored (no frontMatterError)', async () => {
90
+ fetchSpy.mockResolvedValue(mockResponse(200, {
91
+ result: {
92
+ id: 'doc-2',
93
+ path: '/imported/my-doc',
94
+ outcome: 'created',
95
+ existed: false,
96
+ name: 'my-doc',
97
+ frontMatterIgnored: true,
98
+ },
99
+ }));
100
+ const log = await runLocalGrammarMigration(fakeSource(), BASE_OPTIONS);
101
+ expect(warnSpy).toHaveBeenCalledOnce();
102
+ expect(log.summary.fmIgnoredOrDegraded).toBe(1);
103
+ });
104
+ it('fmIgnoredOrDegraded is absent from summary when no FM issues occurred', async () => {
105
+ fetchSpy.mockResolvedValue(mockResponse(200, {
106
+ result: {
107
+ id: 'doc-3',
108
+ path: '/imported/clean',
109
+ outcome: 'created',
110
+ existed: false,
111
+ name: 'clean',
112
+ frontMatterIgnored: false,
113
+ },
114
+ }));
115
+ const log = await runLocalGrammarMigration(fakeSource(), BASE_OPTIONS);
116
+ expect(warnSpy).not.toHaveBeenCalled();
117
+ expect(log.summary.fmIgnoredOrDegraded).toBeUndefined();
118
+ });
119
+ });
package/dist/index.js CHANGED
@@ -142,6 +142,9 @@ function printSummary(log) {
142
142
  console.log(`Updated: ${log.summary.updated}`);
143
143
  console.log(`Skipped: ${log.summary.skipped}`);
144
144
  console.log(`Failed: ${log.summary.failed}`);
145
+ if (log.summary.fmIgnoredOrDegraded) {
146
+ console.log(`FM ignored: ${log.summary.fmIgnoredOrDegraded} (see warnings above)`);
147
+ }
145
148
  if (log.options.dryRun) {
146
149
  console.log('\n(Dry run - no changes made)');
147
150
  }
@@ -7,6 +7,13 @@ import * as fs from 'fs/promises';
7
7
  import * as fsSync from 'fs';
8
8
  import * as path from 'path';
9
9
  import { glob } from 'glob';
10
+ // NAMED import, matching this package's OWN dependency (unified@^11, pure ESM,
11
+ // named export only — no default). The published artifact resolves ^11; a
12
+ // default import bricks the npx-installed CLI at load time (proven: 0.4.40).
13
+ // In-repo gotcha: this package is STANDALONE-installed (own package-lock.json)
14
+ // — run `npm install` INSIDE packages/kb-migrate before building, or tsc/tsx
15
+ // resolve the monorepo ROOT's hoisted CJS unified@9 and this import appears
16
+ // broken (TS2595). That mirage is what shipped the 0.4.40 regression.
10
17
  import { unified } from 'unified';
11
18
  import remarkParse from 'remark-parse';
12
19
  import { MigrationSource } from './base.js';
package/dist/types.d.ts CHANGED
@@ -176,6 +176,12 @@ export interface MigrationLog {
176
176
  discovered?: number;
177
177
  /** Pages skipped during extraction (empty, no content, errors). */
178
178
  skippedDuringExtraction?: number;
179
+ /**
180
+ * Number of files where front-matter was ignored (no name) or degraded
181
+ * (strict YAML failed but lenient fallback recovered a name). Non-zero means
182
+ * some files lost tags/description/properties declared in the front-matter.
183
+ */
184
+ fmIgnoredOrDegraded?: number;
179
185
  };
180
186
  }
181
187
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxn/kb-migrate",
3
- "version": "0.4.39",
3
+ "version": "0.4.41",
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",
@@ -155,4 +155,4 @@
155
155
  "publishConfig": {
156
156
  "access": "public"
157
157
  }
158
- }
158
+ }