@nebutra/document-pipeline 0.1.0 → 0.1.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.
@@ -1,6 +1,6 @@
1
- {"at":"2026-05-18T05:13:30.832Z","type":"reindex","tenantId":"tenant_a","files":0}
2
- {"at":"2026-05-18T05:13:30.913Z","type":"write","tenantId":"tenant_a","path":"research/BRAND.md"}
3
- {"at":"2026-05-18T05:13:30.970Z","type":"reindex","tenantId":"tenant_a","files":0}
4
- {"at":"2026-05-18T05:13:31.034Z","type":"reindex","tenantId":"tenant_a","files":0}
5
- {"at":"2026-05-18T05:13:31.113Z","type":"reindex","tenantId":"tenant_a","files":0}
6
- {"at":"2026-05-18T05:13:31.161Z","type":"write","tenantId":"tenant_a","path":"notes/a.md"}
1
+ {"at":"2026-05-19T08:15:10.701Z","type":"reindex","tenantId":"tenant_a","files":0}
2
+ {"at":"2026-05-19T08:15:10.749Z","type":"write","tenantId":"tenant_a","path":"research/BRAND.md"}
3
+ {"at":"2026-05-19T08:15:10.827Z","type":"reindex","tenantId":"tenant_a","files":0}
4
+ {"at":"2026-05-19T08:15:10.866Z","type":"reindex","tenantId":"tenant_a","files":0}
5
+ {"at":"2026-05-19T08:15:10.894Z","type":"reindex","tenantId":"tenant_a","files":0}
6
+ {"at":"2026-05-19T08:15:10.898Z","type":"write","tenantId":"tenant_a","path":"notes/a.md"}
@@ -1,3 +1,3 @@
1
- {"at":"2026-05-18T05:13:30.841Z","type":"parse","tenantId":"tenant_a","parser":"markdown","path":"research/BRAND.md","chunks":1}
2
- {"at":"2026-05-18T05:13:30.914Z","type":"ingest","tenantId":"tenant_a","path":"research/BRAND.md","parser":"markdown","chunks":1}
3
- {"at":"2026-05-18T05:13:30.973Z","type":"parse","tenantId":"tenant_a","parser":"html","path":"page.html","chunks":1}
1
+ {"at":"2026-05-19T08:15:10.708Z","type":"parse","tenantId":"tenant_a","parser":"markdown","path":"research/BRAND.md","chunks":1}
2
+ {"at":"2026-05-19T08:15:10.750Z","type":"ingest","tenantId":"tenant_a","path":"research/BRAND.md","parser":"markdown","chunks":1}
3
+ {"at":"2026-05-19T08:15:10.832Z","type":"parse","tenantId":"tenant_a","parser":"html","path":"page.html","chunks":1}
@@ -1,5 +1,5 @@
1
1
 
2
- > @nebutra/document-pipeline@0.1.0 build /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/document-pipeline
2
+ > @nebutra/document-pipeline@0.1.1 build /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/document-pipeline
3
3
  > tsup
4
4
 
5
5
  CLI Building entry: src/cli.ts, src/index.ts
@@ -11,12 +11,12 @@
11
11
  ESM Build start
12
12
  ESM dist/cli.js 1.10 KB
13
13
  ESM dist/index.js 180.00 B
14
- ESM dist/chunk-HZF62UJB.js 8.41 KB
14
+ ESM dist/chunk-GC4W5KJ2.js 7.81 KB
15
15
  ESM dist/cli.js.map 1.93 KB
16
16
  ESM dist/index.js.map 71.00 B
17
- ESM dist/chunk-HZF62UJB.js.map 18.54 KB
18
- ESM ⚡️ Build success in 91ms
17
+ ESM dist/chunk-GC4W5KJ2.js.map 16.66 KB
18
+ ESM ⚡️ Build success in 109ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 9029ms
20
+ DTS ⚡️ Build success in 10840ms
21
21
  DTS dist/cli.d.ts 13.00 B
22
22
  DTS dist/index.d.ts 3.44 KB
@@ -1,16 +1,14 @@
1
1
 
2
- > @nebutra/document-pipeline@0.1.0 test /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/document-pipeline
2
+ > @nebutra/document-pipeline@0.1.1 test /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/document-pipeline
3
3
  > vitest run
4
4
 
5
5
 
6
6
   RUN  v4.1.4 /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/document-pipeline
7
7
 
8
- (node:8589) ExperimentalWarning: SQLite is an experimental feature and might change at any time
9
- (Use `node --trace-warnings ...` to show where the warning was created)
10
- ✓ src/index.test.ts (5 tests) 546ms
8
+ ✓ src/index.test.ts (5 tests) 372ms
11
9
 
12
10
   Test Files  1 passed (1)
13
11
   Tests  5 passed (5)
14
-  Start at  05:13:29
15
-  Duration  2.06s (transform 688ms, setup 0ms, import 837ms, tests 546ms, environment 0ms)
12
+  Start at  08:15:09
13
+  Duration  1.31s (transform 563ms, setup 0ms, import 647ms, tests 372ms, environment 0ms)
16
14
 
@@ -1,4 +1,4 @@
1
1
 
2
- > @nebutra/document-pipeline@0.1.0 typecheck /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/document-pipeline
2
+ > @nebutra/document-pipeline@0.1.1 typecheck /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/document-pipeline
3
3
  > tsc --noEmit
4
4
 
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @nebutra/document-pipeline
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Publish registry package metadata under the MIT license.
8
+
9
+ - Updated dependencies []:
10
+ - @nebutra/capability-kit@0.2.1
11
+ - @nebutra/content-store@0.1.1
12
+ - @nebutra/errors@0.1.1
13
+ - @nebutra/sandbox-runtime@0.1.1
@@ -1,36 +1,17 @@
1
1
  // src/index.ts
2
- import { mkdir, readFile, writeFile } from "fs/promises";
3
- import { dirname, extname, join } from "path";
4
- import { ContentStore } from "@nebutra/content-store";
2
+ import { readFile } from "fs/promises";
3
+ import { extname, join } from "path";
4
+ import { requireCapabilityTenant } from "@nebutra/capability-kit";
5
+ import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
6
+ import {
7
+ ContentStore,
8
+ parseContentFrontmatter,
9
+ serializeContentFrontmatter,
10
+ splitContentParagraphs
11
+ } from "@nebutra/content-store";
5
12
  import { CapabilityError } from "@nebutra/errors";
6
- function debugPath(root = process.cwd()) {
7
- return join(root, ".nebutra", "debug", "document-pipeline.jsonl");
8
- }
9
- async function appendDebug(root, entry) {
10
- const path = debugPath(root);
11
- await mkdir(dirname(path), { recursive: true });
12
- await writeFile(path, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
13
- `, {
14
- flag: "a"
15
- });
16
- }
17
13
  async function readDocumentPipelineDebug(root = process.cwd(), limit = 20) {
18
- try {
19
- const raw = await readFile(debugPath(root), "utf8");
20
- return raw.trim().split("\n").filter(Boolean).slice(-limit).map((line) => JSON.parse(line));
21
- } catch {
22
- return [];
23
- }
24
- }
25
- function requireTenant(explicit, fallback) {
26
- const tenantId = explicit ?? fallback;
27
- if (!tenantId) {
28
- throw new CapabilityError("document-pipeline", "Document ingestion requires tenant context", {
29
- suggestion: "Pass tenantId on the request or construct DocumentPipeline with a tenantId default.",
30
- statusCode: 400
31
- });
32
- }
33
- return tenantId;
14
+ return readCapabilityDebug("document-pipeline", { root, limit });
34
15
  }
35
16
  var DocumentPipeline = class _DocumentPipeline {
36
17
  #tenantId;
@@ -51,7 +32,14 @@ var DocumentPipeline = class _DocumentPipeline {
51
32
  return new _DocumentPipeline({ ...options, tenantId, root, contentStore });
52
33
  }
53
34
  async parse(request) {
54
- const tenantId = requireTenant(request.tenantId, this.#tenantId);
35
+ const tenantId = requireCapabilityTenant({
36
+ explicit: request.tenantId,
37
+ fallback: this.#tenantId,
38
+ onMissing: () => new CapabilityError("document-pipeline", "Document ingestion requires tenant context", {
39
+ suggestion: "Pass tenantId on the request or construct DocumentPipeline with a tenantId default.",
40
+ statusCode: 400
41
+ })
42
+ });
55
43
  const required = { ...request, tenantId };
56
44
  const parser = this.#chooseParser(required);
57
45
  if (parser === "sidecar") {
@@ -63,39 +51,58 @@ var DocumentPipeline = class _DocumentPipeline {
63
51
  });
64
52
  }
65
53
  const parsed2 = await this.#sidecarParser.parse(required);
66
- await appendDebug(this.#debugRoot, {
67
- type: "parse",
68
- tenantId,
69
- parser: "sidecar",
70
- path: parsed2.path
71
- });
54
+ await appendCapabilityDebug(
55
+ "document-pipeline",
56
+ {
57
+ type: "parse",
58
+ tenantId,
59
+ parser: "sidecar",
60
+ path: parsed2.path
61
+ },
62
+ { root: this.#debugRoot }
63
+ );
72
64
  return parsed2;
73
65
  }
74
66
  const raw = await readSource(request.source);
75
67
  const parsed = parseNativeDocument(required, raw, parser);
76
- await appendDebug(this.#debugRoot, {
77
- type: "parse",
78
- tenantId,
79
- parser,
80
- path: parsed.path,
81
- chunks: parsed.chunks.length
82
- });
68
+ await appendCapabilityDebug(
69
+ "document-pipeline",
70
+ {
71
+ type: "parse",
72
+ tenantId,
73
+ parser,
74
+ path: parsed.path,
75
+ chunks: parsed.chunks.length
76
+ },
77
+ { root: this.#debugRoot }
78
+ );
83
79
  return parsed;
84
80
  }
85
81
  async ingest(request) {
86
- const tenantId = requireTenant(request.tenantId, this.#tenantId);
82
+ const tenantId = requireCapabilityTenant({
83
+ explicit: request.tenantId,
84
+ fallback: this.#tenantId,
85
+ onMissing: () => new CapabilityError("document-pipeline", "Document ingestion requires tenant context", {
86
+ suggestion: "Pass tenantId on the request or construct DocumentPipeline with a tenantId default.",
87
+ statusCode: 400
88
+ })
89
+ });
87
90
  const contentStore = await this.#resolveContentStore(tenantId);
88
91
  const parsed = await this.parse({ ...request, tenantId });
89
92
  const targetPath = request.targetPath ?? parsed.path;
90
93
  const content = serializeForContentStore(parsed, request.metadata);
91
94
  await contentStore.write(targetPath, content);
92
- await appendDebug(this.#debugRoot, {
93
- type: "ingest",
94
- tenantId,
95
- path: targetPath,
96
- parser: parsed.parser,
97
- chunks: parsed.chunks.length
98
- });
95
+ await appendCapabilityDebug(
96
+ "document-pipeline",
97
+ {
98
+ type: "ingest",
99
+ tenantId,
100
+ path: targetPath,
101
+ parser: parsed.parser,
102
+ chunks: parsed.chunks.length
103
+ },
104
+ { root: this.#debugRoot }
105
+ );
99
106
  return {
100
107
  tenantId,
101
108
  path: targetPath,
@@ -151,9 +158,9 @@ function mimeFromPath(path) {
151
158
  }
152
159
  function parseNativeDocument(request, raw, parser) {
153
160
  const mimeType = request.source.mimeType ?? mimeFromPath(request.source.path);
154
- const { frontmatter, body } = parser === "markdown" ? parseFrontmatter(raw) : { frontmatter: {}, body: raw };
161
+ const { frontmatter, body } = parser === "markdown" ? parseContentFrontmatter(raw) : { frontmatter: {}, body: raw };
155
162
  const text = parser === "html" ? htmlToText(body) : body;
156
- const chunks = chunkParagraphs(text).map((chunk, index) => ({
163
+ const chunks = splitContentParagraphs(text).map((chunk, index) => ({
157
164
  id: `${request.tenantId}:${request.source.path}:${index}`,
158
165
  text: chunk,
159
166
  metadata: {
@@ -194,27 +201,8 @@ function serializeForContentStore(parsed, metadata) {
194
201
  source_mime: parsed.mimeType,
195
202
  parser: parsed.parser
196
203
  };
197
- const frontmatterLines = Object.entries(frontmatter).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}: ${value}`);
198
204
  const body = parsed.chunks.map((chunk) => chunk.text).join("\n\n");
199
- return frontmatterLines.length > 0 ? `---
200
- ${frontmatterLines.join("\n")}
201
- ---
202
- ${body}` : body;
203
- }
204
- function parseFrontmatter(content) {
205
- if (!content.startsWith("---\n")) return { frontmatter: {}, body: content };
206
- const end = content.indexOf("\n---", 4);
207
- if (end < 0) return { frontmatter: {}, body: content };
208
- const raw = content.slice(4, end).trim();
209
- const frontmatter = {};
210
- for (const line of raw.split("\n")) {
211
- const idx = line.indexOf(":");
212
- if (idx > 0) frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
213
- }
214
- return { frontmatter, body: content.slice(end + 4).trimStart() };
215
- }
216
- function chunkParagraphs(content) {
217
- return content.split(/\n\s*\n/).map((part) => part.trim()).filter(Boolean);
205
+ return serializeContentFrontmatter(frontmatter, body);
218
206
  }
219
207
  function htmlToText(html) {
220
208
  return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<\/(h[1-6]|p|li|tr|div)>/gi, "\n\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/[ \t]+/g, " ").replace(/\n\s+/g, "\n").trim();
@@ -224,4 +212,4 @@ export {
224
212
  readDocumentPipelineDebug,
225
213
  DocumentPipeline
226
214
  };
227
- //# sourceMappingURL=chunk-HZF62UJB.js.map
215
+ //# sourceMappingURL=chunk-GC4W5KJ2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { extname, join } from \"node:path\";\nimport { requireCapabilityTenant } from \"@nebutra/capability-kit\";\nimport { appendCapabilityDebug, readCapabilityDebug } from \"@nebutra/capability-kit/debug\";\nimport {\n ContentStore,\n parseContentFrontmatter,\n serializeContentFrontmatter,\n splitContentParagraphs,\n} from \"@nebutra/content-store\";\nimport { CapabilityError } from \"@nebutra/errors\";\n\nexport type ParserChoice = \"auto\" | \"markdown\" | \"html\" | \"text\" | \"sidecar\";\nexport type ChunkerChoice = \"paragraph\" | \"recursive\" | \"hierarchical\";\n\nexport interface InlineSource {\n readonly type: \"inline\";\n readonly path: string;\n readonly content: string;\n readonly mimeType?: string;\n}\n\nexport interface FileSource {\n readonly type: \"file\";\n readonly path: string;\n readonly mimeType?: string;\n}\n\nexport type DocumentSource = InlineSource | FileSource;\n\nexport interface IngestRequest {\n readonly tenantId?: string;\n readonly source: DocumentSource;\n readonly parser?: ParserChoice;\n readonly chunker?: ChunkerChoice;\n readonly targetPath?: string;\n readonly metadata?: Record<string, string>;\n}\n\nexport interface ParseRequest {\n readonly tenantId?: string;\n readonly source: DocumentSource;\n readonly parser?: ParserChoice;\n readonly chunker?: ChunkerChoice;\n readonly metadata?: Record<string, string>;\n}\n\nexport interface ParsedChunk {\n readonly id: string;\n readonly text: string;\n readonly metadata: {\n readonly tenantId: string;\n readonly sourcePath: string;\n readonly chunkIndex: number;\n readonly mimeType: string;\n readonly parser: ParserChoice;\n readonly heading?: string;\n } & Record<string, string | number>;\n}\n\nexport interface ParsedDocument {\n readonly tenantId: string;\n readonly path: string;\n readonly mimeType: string;\n readonly parser: ParserChoice;\n readonly chunks: readonly ParsedChunk[];\n readonly frontmatter: Record<string, string>;\n}\n\nexport interface IngestResult {\n readonly tenantId: string;\n readonly path: string;\n readonly parser: ParserChoice;\n readonly chunkCount: number;\n readonly contentIndexed: true;\n}\n\nexport interface DocumentPipelineDoctorReport {\n readonly capability: \"document-pipeline\";\n readonly nativeParsers: readonly ParserChoice[];\n readonly sidecarConfigured: boolean;\n readonly contentStore: Awaited<ReturnType<ContentStore[\"doctor\"]>>;\n readonly suggestion?: string;\n}\n\nexport interface DocumentParser {\n parse(request: RequiredTenant<ParseRequest>): Promise<ParsedDocument>;\n doctor(): Promise<{ readonly ok: boolean; readonly suggestion?: string }>;\n}\n\nexport interface DocumentPipelineOptions {\n readonly tenantId?: string;\n readonly root?: string;\n readonly debugRoot?: string;\n readonly contentStore?: ContentStore;\n readonly sidecarParser?: DocumentParser;\n}\n\ntype RequiredTenant<T extends { readonly tenantId?: string }> = Omit<T, \"tenantId\"> & {\n readonly tenantId: string;\n};\n\nexport async function readDocumentPipelineDebug(\n root = process.cwd(),\n limit = 20,\n): Promise<unknown[]> {\n return readCapabilityDebug(\"document-pipeline\", { root, limit });\n}\n\nexport class DocumentPipeline {\n readonly #tenantId: string | undefined;\n readonly #root: string;\n readonly #debugRoot: string;\n readonly #contentStore: ContentStore | undefined;\n readonly #sidecarParser: DocumentParser | undefined;\n\n constructor(options: DocumentPipelineOptions = {}) {\n this.#tenantId = options.tenantId;\n this.#root = options.root ?? join(process.cwd(), \".nebutra\", \"document-pipeline\");\n this.#debugRoot = options.debugRoot ?? process.cwd();\n this.#contentStore = options.contentStore;\n this.#sidecarParser = options.sidecarParser;\n }\n\n static async open(\n root = \".nebutra/document-pipeline\",\n options: Omit<DocumentPipelineOptions, \"root\" | \"contentStore\"> = {},\n ): Promise<DocumentPipeline> {\n const tenantId = options.tenantId ?? \"local\";\n const contentStore = await ContentStore.open(join(root, \"content\"), { tenantId });\n return new DocumentPipeline({ ...options, tenantId, root, contentStore });\n }\n\n async parse(request: ParseRequest): Promise<ParsedDocument> {\n const tenantId = requireCapabilityTenant({\n explicit: request.tenantId,\n fallback: this.#tenantId,\n onMissing: () =>\n new CapabilityError(\"document-pipeline\", \"Document ingestion requires tenant context\", {\n suggestion:\n \"Pass tenantId on the request or construct DocumentPipeline with a tenantId default.\",\n statusCode: 400,\n }),\n });\n const required: RequiredTenant<ParseRequest> = { ...request, tenantId };\n const parser = this.#chooseParser(required);\n if (parser === \"sidecar\") {\n if (!this.#sidecarParser) {\n throw new CapabilityError(\"document-pipeline\", \"Parser sidecar is not configured\", {\n suggestion:\n \"Configure the parser sidecar for PDFs, office files, OCR, and layout-aware extraction.\",\n statusCode: 503,\n metadata: { path: request.source.path },\n });\n }\n const parsed = await this.#sidecarParser.parse(required);\n await appendCapabilityDebug(\n \"document-pipeline\",\n {\n type: \"parse\",\n tenantId,\n parser: \"sidecar\",\n path: parsed.path,\n },\n { root: this.#debugRoot },\n );\n return parsed;\n }\n\n const raw = await readSource(request.source);\n const parsed = parseNativeDocument(required, raw, parser);\n await appendCapabilityDebug(\n \"document-pipeline\",\n {\n type: \"parse\",\n tenantId,\n parser,\n path: parsed.path,\n chunks: parsed.chunks.length,\n },\n { root: this.#debugRoot },\n );\n return parsed;\n }\n\n async ingest(request: IngestRequest): Promise<IngestResult> {\n const tenantId = requireCapabilityTenant({\n explicit: request.tenantId,\n fallback: this.#tenantId,\n onMissing: () =>\n new CapabilityError(\"document-pipeline\", \"Document ingestion requires tenant context\", {\n suggestion:\n \"Pass tenantId on the request or construct DocumentPipeline with a tenantId default.\",\n statusCode: 400,\n }),\n });\n const contentStore = await this.#resolveContentStore(tenantId);\n const parsed = await this.parse({ ...request, tenantId });\n const targetPath = request.targetPath ?? parsed.path;\n const content = serializeForContentStore(parsed, request.metadata);\n await contentStore.write(targetPath, content);\n await appendCapabilityDebug(\n \"document-pipeline\",\n {\n type: \"ingest\",\n tenantId,\n path: targetPath,\n parser: parsed.parser,\n chunks: parsed.chunks.length,\n },\n { root: this.#debugRoot },\n );\n return {\n tenantId,\n path: targetPath,\n parser: parsed.parser,\n chunkCount: parsed.chunks.length,\n contentIndexed: true,\n };\n }\n\n async ingestFile(\n path: string,\n options: Omit<IngestRequest, \"source\"> = {},\n ): Promise<IngestResult> {\n return this.ingest({ ...options, source: { type: \"file\", path } });\n }\n\n async doctor(): Promise<DocumentPipelineDoctorReport> {\n const contentStore = await this.#resolveContentStore(this.#tenantId ?? \"local\");\n const sidecar = this.#sidecarParser ? await this.#sidecarParser.doctor() : { ok: false };\n return {\n capability: \"document-pipeline\",\n nativeParsers: [\"markdown\", \"html\", \"text\"],\n sidecarConfigured: sidecar.ok,\n contentStore: await contentStore.doctor(),\n ...(sidecar.ok\n ? {}\n : {\n suggestion:\n \"Native parsers are active; configure the parser sidecar before ingesting PDFs, office files, OCR, or complex layout documents.\",\n }),\n };\n }\n\n #chooseParser(request: RequiredTenant<ParseRequest>): ParserChoice {\n if (request.parser && request.parser !== \"auto\") return request.parser;\n const mime = request.source.mimeType ?? mimeFromPath(request.source.path);\n if (mime === \"text/markdown\") return \"markdown\";\n if (mime === \"text/html\") return \"html\";\n if (mime.startsWith(\"text/\")) return \"text\";\n return \"sidecar\";\n }\n\n async #resolveContentStore(tenantId: string): Promise<ContentStore> {\n if (this.#contentStore) return this.#contentStore;\n return ContentStore.open(join(this.#root, \"content\"), { tenantId });\n }\n}\n\nasync function readSource(source: DocumentSource): Promise<string> {\n return source.type === \"inline\" ? source.content : readFile(source.path, \"utf8\");\n}\n\nfunction mimeFromPath(path: string): string {\n const extension = extname(path).toLowerCase();\n if (extension === \".md\" || extension === \".mdx\") return \"text/markdown\";\n if (extension === \".html\" || extension === \".htm\") return \"text/html\";\n if (extension === \".txt\" || extension === \".csv\" || extension === \".json\") return \"text/plain\";\n if (extension === \".pdf\") return \"application/pdf\";\n if (extension === \".docx\")\n return \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\";\n if (extension === \".pptx\")\n return \"application/vnd.openxmlformats-officedocument.presentationml.presentation\";\n if (extension === \".png\" || extension === \".jpg\" || extension === \".jpeg\") return \"image\";\n return \"application/octet-stream\";\n}\n\nfunction parseNativeDocument(\n request: RequiredTenant<ParseRequest>,\n raw: string,\n parser: ParserChoice,\n): ParsedDocument {\n const mimeType = request.source.mimeType ?? mimeFromPath(request.source.path);\n const { frontmatter, body } =\n parser === \"markdown\" ? parseContentFrontmatter(raw) : { frontmatter: {}, body: raw };\n const text = parser === \"html\" ? htmlToText(body) : body;\n const chunks = splitContentParagraphs(text).map((chunk, index) => ({\n id: `${request.tenantId}:${request.source.path}:${index}`,\n text: chunk,\n metadata: {\n tenantId: request.tenantId,\n sourcePath: request.source.path,\n chunkIndex: index,\n mimeType,\n parser,\n ...(request.metadata ?? {}),\n },\n }));\n return {\n tenantId: request.tenantId,\n path: request.source.path,\n mimeType,\n parser,\n chunks:\n chunks.length > 0\n ? chunks\n : [\n {\n id: `${request.tenantId}:${request.source.path}:0`,\n text,\n metadata: {\n tenantId: request.tenantId,\n sourcePath: request.source.path,\n chunkIndex: 0,\n mimeType,\n parser,\n ...(request.metadata ?? {}),\n },\n },\n ],\n frontmatter,\n };\n}\n\nfunction serializeForContentStore(\n parsed: ParsedDocument,\n metadata: Record<string, string> | undefined,\n): string {\n const frontmatter = {\n ...parsed.frontmatter,\n ...(metadata ?? {}),\n source_mime: parsed.mimeType,\n parser: parsed.parser,\n };\n const body = parsed.chunks.map((chunk) => chunk.text).join(\"\\n\\n\");\n return serializeContentFrontmatter(frontmatter, body);\n}\n\nfunction htmlToText(html: string): string {\n return html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<\\/(h[1-6]|p|li|tr|div)>/gi, \"\\n\\n\")\n .replace(/<[^>]+>/g, \" \")\n .replace(/&nbsp;/g, \" \")\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/[ \\t]+/g, \" \")\n .replace(/\\n\\s+/g, \"\\n\")\n .trim();\n}\n"],"mappings":"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","names":["parsed"]}
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DocumentPipeline,
3
3
  readDocumentPipelineDebug
4
- } from "./chunk-HZF62UJB.js";
4
+ } from "./chunk-GC4W5KJ2.js";
5
5
 
6
6
  // src/cli.ts
7
7
  var command = process.argv[2] ?? "doctor";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DocumentPipeline,
3
3
  readDocumentPipelineDebug
4
- } from "./chunk-HZF62UJB.js";
4
+ } from "./chunk-GC4W5KJ2.js";
5
5
  export {
6
6
  DocumentPipeline,
7
7
  readDocumentPipelineDebug
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebutra/document-pipeline",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Document parse, chunk, embed-port, and content-store ingestion pipeline",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -28,9 +28,10 @@
28
28
  ".": "./src/index.ts"
29
29
  },
30
30
  "dependencies": {
31
- "@nebutra/content-store": "0.1.0",
32
- "@nebutra/errors": "0.1.0",
33
- "@nebutra/sandbox-runtime": "0.1.0"
31
+ "@nebutra/capability-kit": "0.2.1",
32
+ "@nebutra/content-store": "0.1.1",
33
+ "@nebutra/errors": "0.1.1",
34
+ "@nebutra/sandbox-runtime": "0.1.1"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/node": "^22.19.15",
package/src/index.ts CHANGED
@@ -1,6 +1,13 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, extname, join } from "node:path";
3
- import { ContentStore } from "@nebutra/content-store";
1
+ import { readFile } from "node:fs/promises";
2
+ import { extname, join } from "node:path";
3
+ import { requireCapabilityTenant } from "@nebutra/capability-kit";
4
+ import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
5
+ import {
6
+ ContentStore,
7
+ parseContentFrontmatter,
8
+ serializeContentFrontmatter,
9
+ splitContentParagraphs,
10
+ } from "@nebutra/content-store";
4
11
  import { CapabilityError } from "@nebutra/errors";
5
12
 
6
13
  export type ParserChoice = "auto" | "markdown" | "html" | "text" | "sidecar";
@@ -93,45 +100,11 @@ type RequiredTenant<T extends { readonly tenantId?: string }> = Omit<T, "tenantI
93
100
  readonly tenantId: string;
94
101
  };
95
102
 
96
- function debugPath(root = process.cwd()): string {
97
- return join(root, ".nebutra", "debug", "document-pipeline.jsonl");
98
- }
99
-
100
- async function appendDebug(root: string, entry: Record<string, unknown>): Promise<void> {
101
- const path = debugPath(root);
102
- await mkdir(dirname(path), { recursive: true });
103
- await writeFile(path, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`, {
104
- flag: "a",
105
- });
106
- }
107
-
108
103
  export async function readDocumentPipelineDebug(
109
104
  root = process.cwd(),
110
105
  limit = 20,
111
106
  ): Promise<unknown[]> {
112
- try {
113
- const raw = await readFile(debugPath(root), "utf8");
114
- return raw
115
- .trim()
116
- .split("\n")
117
- .filter(Boolean)
118
- .slice(-limit)
119
- .map((line) => JSON.parse(line) as unknown);
120
- } catch {
121
- return [];
122
- }
123
- }
124
-
125
- function requireTenant(explicit: string | undefined, fallback: string | undefined): string {
126
- const tenantId = explicit ?? fallback;
127
- if (!tenantId) {
128
- throw new CapabilityError("document-pipeline", "Document ingestion requires tenant context", {
129
- suggestion:
130
- "Pass tenantId on the request or construct DocumentPipeline with a tenantId default.",
131
- statusCode: 400,
132
- });
133
- }
134
- return tenantId;
107
+ return readCapabilityDebug("document-pipeline", { root, limit });
135
108
  }
136
109
 
137
110
  export class DocumentPipeline {
@@ -159,7 +132,16 @@ export class DocumentPipeline {
159
132
  }
160
133
 
161
134
  async parse(request: ParseRequest): Promise<ParsedDocument> {
162
- const tenantId = requireTenant(request.tenantId, this.#tenantId);
135
+ const tenantId = requireCapabilityTenant({
136
+ explicit: request.tenantId,
137
+ fallback: this.#tenantId,
138
+ onMissing: () =>
139
+ new CapabilityError("document-pipeline", "Document ingestion requires tenant context", {
140
+ suggestion:
141
+ "Pass tenantId on the request or construct DocumentPipeline with a tenantId default.",
142
+ statusCode: 400,
143
+ }),
144
+ });
163
145
  const required: RequiredTenant<ParseRequest> = { ...request, tenantId };
164
146
  const parser = this.#chooseParser(required);
165
147
  if (parser === "sidecar") {
@@ -172,41 +154,62 @@ export class DocumentPipeline {
172
154
  });
173
155
  }
174
156
  const parsed = await this.#sidecarParser.parse(required);
175
- await appendDebug(this.#debugRoot, {
176
- type: "parse",
177
- tenantId,
178
- parser: "sidecar",
179
- path: parsed.path,
180
- });
157
+ await appendCapabilityDebug(
158
+ "document-pipeline",
159
+ {
160
+ type: "parse",
161
+ tenantId,
162
+ parser: "sidecar",
163
+ path: parsed.path,
164
+ },
165
+ { root: this.#debugRoot },
166
+ );
181
167
  return parsed;
182
168
  }
183
169
 
184
170
  const raw = await readSource(request.source);
185
171
  const parsed = parseNativeDocument(required, raw, parser);
186
- await appendDebug(this.#debugRoot, {
187
- type: "parse",
188
- tenantId,
189
- parser,
190
- path: parsed.path,
191
- chunks: parsed.chunks.length,
192
- });
172
+ await appendCapabilityDebug(
173
+ "document-pipeline",
174
+ {
175
+ type: "parse",
176
+ tenantId,
177
+ parser,
178
+ path: parsed.path,
179
+ chunks: parsed.chunks.length,
180
+ },
181
+ { root: this.#debugRoot },
182
+ );
193
183
  return parsed;
194
184
  }
195
185
 
196
186
  async ingest(request: IngestRequest): Promise<IngestResult> {
197
- const tenantId = requireTenant(request.tenantId, this.#tenantId);
187
+ const tenantId = requireCapabilityTenant({
188
+ explicit: request.tenantId,
189
+ fallback: this.#tenantId,
190
+ onMissing: () =>
191
+ new CapabilityError("document-pipeline", "Document ingestion requires tenant context", {
192
+ suggestion:
193
+ "Pass tenantId on the request or construct DocumentPipeline with a tenantId default.",
194
+ statusCode: 400,
195
+ }),
196
+ });
198
197
  const contentStore = await this.#resolveContentStore(tenantId);
199
198
  const parsed = await this.parse({ ...request, tenantId });
200
199
  const targetPath = request.targetPath ?? parsed.path;
201
200
  const content = serializeForContentStore(parsed, request.metadata);
202
201
  await contentStore.write(targetPath, content);
203
- await appendDebug(this.#debugRoot, {
204
- type: "ingest",
205
- tenantId,
206
- path: targetPath,
207
- parser: parsed.parser,
208
- chunks: parsed.chunks.length,
209
- });
202
+ await appendCapabilityDebug(
203
+ "document-pipeline",
204
+ {
205
+ type: "ingest",
206
+ tenantId,
207
+ path: targetPath,
208
+ parser: parsed.parser,
209
+ chunks: parsed.chunks.length,
210
+ },
211
+ { root: this.#debugRoot },
212
+ );
210
213
  return {
211
214
  tenantId,
212
215
  path: targetPath,
@@ -280,9 +283,9 @@ function parseNativeDocument(
280
283
  ): ParsedDocument {
281
284
  const mimeType = request.source.mimeType ?? mimeFromPath(request.source.path);
282
285
  const { frontmatter, body } =
283
- parser === "markdown" ? parseFrontmatter(raw) : { frontmatter: {}, body: raw };
286
+ parser === "markdown" ? parseContentFrontmatter(raw) : { frontmatter: {}, body: raw };
284
287
  const text = parser === "html" ? htmlToText(body) : body;
285
- const chunks = chunkParagraphs(text).map((chunk, index) => ({
288
+ const chunks = splitContentParagraphs(text).map((chunk, index) => ({
286
289
  id: `${request.tenantId}:${request.source.path}:${index}`,
287
290
  text: chunk,
288
291
  metadata: {
@@ -330,34 +333,8 @@ function serializeForContentStore(
330
333
  source_mime: parsed.mimeType,
331
334
  parser: parsed.parser,
332
335
  };
333
- const frontmatterLines = Object.entries(frontmatter)
334
- .sort(([left], [right]) => left.localeCompare(right))
335
- .map(([key, value]) => `${key}: ${value}`);
336
336
  const body = parsed.chunks.map((chunk) => chunk.text).join("\n\n");
337
- return frontmatterLines.length > 0 ? `---\n${frontmatterLines.join("\n")}\n---\n${body}` : body;
338
- }
339
-
340
- function parseFrontmatter(content: string): {
341
- readonly frontmatter: Record<string, string>;
342
- readonly body: string;
343
- } {
344
- if (!content.startsWith("---\n")) return { frontmatter: {}, body: content };
345
- const end = content.indexOf("\n---", 4);
346
- if (end < 0) return { frontmatter: {}, body: content };
347
- const raw = content.slice(4, end).trim();
348
- const frontmatter: Record<string, string> = {};
349
- for (const line of raw.split("\n")) {
350
- const idx = line.indexOf(":");
351
- if (idx > 0) frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
352
- }
353
- return { frontmatter, body: content.slice(end + 4).trimStart() };
354
- }
355
-
356
- function chunkParagraphs(content: string): string[] {
357
- return content
358
- .split(/\n\s*\n/)
359
- .map((part) => part.trim())
360
- .filter(Boolean);
337
+ return serializeContentFrontmatter(frontmatter, body);
361
338
  }
362
339
 
363
340
  function htmlToText(html: string): string {
package/tsup.config.ts CHANGED
@@ -7,5 +7,10 @@ export default defineConfig({
7
7
  sourcemap: true,
8
8
  clean: true,
9
9
  target: "es2022",
10
- external: ["@nebutra/content-store", "@nebutra/errors", "@nebutra/sandbox-runtime"],
10
+ external: [
11
+ "@nebutra/capability-kit/debug",
12
+ "@nebutra/content-store",
13
+ "@nebutra/errors",
14
+ "@nebutra/sandbox-runtime",
15
+ ],
11
16
  });
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, extname, join } from \"node:path\";\nimport { ContentStore } from \"@nebutra/content-store\";\nimport { CapabilityError } from \"@nebutra/errors\";\n\nexport type ParserChoice = \"auto\" | \"markdown\" | \"html\" | \"text\" | \"sidecar\";\nexport type ChunkerChoice = \"paragraph\" | \"recursive\" | \"hierarchical\";\n\nexport interface InlineSource {\n readonly type: \"inline\";\n readonly path: string;\n readonly content: string;\n readonly mimeType?: string;\n}\n\nexport interface FileSource {\n readonly type: \"file\";\n readonly path: string;\n readonly mimeType?: string;\n}\n\nexport type DocumentSource = InlineSource | FileSource;\n\nexport interface IngestRequest {\n readonly tenantId?: string;\n readonly source: DocumentSource;\n readonly parser?: ParserChoice;\n readonly chunker?: ChunkerChoice;\n readonly targetPath?: string;\n readonly metadata?: Record<string, string>;\n}\n\nexport interface ParseRequest {\n readonly tenantId?: string;\n readonly source: DocumentSource;\n readonly parser?: ParserChoice;\n readonly chunker?: ChunkerChoice;\n readonly metadata?: Record<string, string>;\n}\n\nexport interface ParsedChunk {\n readonly id: string;\n readonly text: string;\n readonly metadata: {\n readonly tenantId: string;\n readonly sourcePath: string;\n readonly chunkIndex: number;\n readonly mimeType: string;\n readonly parser: ParserChoice;\n readonly heading?: string;\n } & Record<string, string | number>;\n}\n\nexport interface ParsedDocument {\n readonly tenantId: string;\n readonly path: string;\n readonly mimeType: string;\n readonly parser: ParserChoice;\n readonly chunks: readonly ParsedChunk[];\n readonly frontmatter: Record<string, string>;\n}\n\nexport interface IngestResult {\n readonly tenantId: string;\n readonly path: string;\n readonly parser: ParserChoice;\n readonly chunkCount: number;\n readonly contentIndexed: true;\n}\n\nexport interface DocumentPipelineDoctorReport {\n readonly capability: \"document-pipeline\";\n readonly nativeParsers: readonly ParserChoice[];\n readonly sidecarConfigured: boolean;\n readonly contentStore: Awaited<ReturnType<ContentStore[\"doctor\"]>>;\n readonly suggestion?: string;\n}\n\nexport interface DocumentParser {\n parse(request: RequiredTenant<ParseRequest>): Promise<ParsedDocument>;\n doctor(): Promise<{ readonly ok: boolean; readonly suggestion?: string }>;\n}\n\nexport interface DocumentPipelineOptions {\n readonly tenantId?: string;\n readonly root?: string;\n readonly debugRoot?: string;\n readonly contentStore?: ContentStore;\n readonly sidecarParser?: DocumentParser;\n}\n\ntype RequiredTenant<T extends { readonly tenantId?: string }> = Omit<T, \"tenantId\"> & {\n readonly tenantId: string;\n};\n\nfunction debugPath(root = process.cwd()): string {\n return join(root, \".nebutra\", \"debug\", \"document-pipeline.jsonl\");\n}\n\nasync function appendDebug(root: string, entry: Record<string, unknown>): Promise<void> {\n const path = debugPath(root);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\\n`, {\n flag: \"a\",\n });\n}\n\nexport async function readDocumentPipelineDebug(\n root = process.cwd(),\n limit = 20,\n): Promise<unknown[]> {\n try {\n const raw = await readFile(debugPath(root), \"utf8\");\n return raw\n .trim()\n .split(\"\\n\")\n .filter(Boolean)\n .slice(-limit)\n .map((line) => JSON.parse(line) as unknown);\n } catch {\n return [];\n }\n}\n\nfunction requireTenant(explicit: string | undefined, fallback: string | undefined): string {\n const tenantId = explicit ?? fallback;\n if (!tenantId) {\n throw new CapabilityError(\"document-pipeline\", \"Document ingestion requires tenant context\", {\n suggestion:\n \"Pass tenantId on the request or construct DocumentPipeline with a tenantId default.\",\n statusCode: 400,\n });\n }\n return tenantId;\n}\n\nexport class DocumentPipeline {\n readonly #tenantId: string | undefined;\n readonly #root: string;\n readonly #debugRoot: string;\n readonly #contentStore: ContentStore | undefined;\n readonly #sidecarParser: DocumentParser | undefined;\n\n constructor(options: DocumentPipelineOptions = {}) {\n this.#tenantId = options.tenantId;\n this.#root = options.root ?? join(process.cwd(), \".nebutra\", \"document-pipeline\");\n this.#debugRoot = options.debugRoot ?? process.cwd();\n this.#contentStore = options.contentStore;\n this.#sidecarParser = options.sidecarParser;\n }\n\n static async open(\n root = \".nebutra/document-pipeline\",\n options: Omit<DocumentPipelineOptions, \"root\" | \"contentStore\"> = {},\n ): Promise<DocumentPipeline> {\n const tenantId = options.tenantId ?? \"local\";\n const contentStore = await ContentStore.open(join(root, \"content\"), { tenantId });\n return new DocumentPipeline({ ...options, tenantId, root, contentStore });\n }\n\n async parse(request: ParseRequest): Promise<ParsedDocument> {\n const tenantId = requireTenant(request.tenantId, this.#tenantId);\n const required: RequiredTenant<ParseRequest> = { ...request, tenantId };\n const parser = this.#chooseParser(required);\n if (parser === \"sidecar\") {\n if (!this.#sidecarParser) {\n throw new CapabilityError(\"document-pipeline\", \"Parser sidecar is not configured\", {\n suggestion:\n \"Configure the parser sidecar for PDFs, office files, OCR, and layout-aware extraction.\",\n statusCode: 503,\n metadata: { path: request.source.path },\n });\n }\n const parsed = await this.#sidecarParser.parse(required);\n await appendDebug(this.#debugRoot, {\n type: \"parse\",\n tenantId,\n parser: \"sidecar\",\n path: parsed.path,\n });\n return parsed;\n }\n\n const raw = await readSource(request.source);\n const parsed = parseNativeDocument(required, raw, parser);\n await appendDebug(this.#debugRoot, {\n type: \"parse\",\n tenantId,\n parser,\n path: parsed.path,\n chunks: parsed.chunks.length,\n });\n return parsed;\n }\n\n async ingest(request: IngestRequest): Promise<IngestResult> {\n const tenantId = requireTenant(request.tenantId, this.#tenantId);\n const contentStore = await this.#resolveContentStore(tenantId);\n const parsed = await this.parse({ ...request, tenantId });\n const targetPath = request.targetPath ?? parsed.path;\n const content = serializeForContentStore(parsed, request.metadata);\n await contentStore.write(targetPath, content);\n await appendDebug(this.#debugRoot, {\n type: \"ingest\",\n tenantId,\n path: targetPath,\n parser: parsed.parser,\n chunks: parsed.chunks.length,\n });\n return {\n tenantId,\n path: targetPath,\n parser: parsed.parser,\n chunkCount: parsed.chunks.length,\n contentIndexed: true,\n };\n }\n\n async ingestFile(\n path: string,\n options: Omit<IngestRequest, \"source\"> = {},\n ): Promise<IngestResult> {\n return this.ingest({ ...options, source: { type: \"file\", path } });\n }\n\n async doctor(): Promise<DocumentPipelineDoctorReport> {\n const contentStore = await this.#resolveContentStore(this.#tenantId ?? \"local\");\n const sidecar = this.#sidecarParser ? await this.#sidecarParser.doctor() : { ok: false };\n return {\n capability: \"document-pipeline\",\n nativeParsers: [\"markdown\", \"html\", \"text\"],\n sidecarConfigured: sidecar.ok,\n contentStore: await contentStore.doctor(),\n ...(sidecar.ok\n ? {}\n : {\n suggestion:\n \"Native parsers are active; configure the parser sidecar before ingesting PDFs, office files, OCR, or complex layout documents.\",\n }),\n };\n }\n\n #chooseParser(request: RequiredTenant<ParseRequest>): ParserChoice {\n if (request.parser && request.parser !== \"auto\") return request.parser;\n const mime = request.source.mimeType ?? mimeFromPath(request.source.path);\n if (mime === \"text/markdown\") return \"markdown\";\n if (mime === \"text/html\") return \"html\";\n if (mime.startsWith(\"text/\")) return \"text\";\n return \"sidecar\";\n }\n\n async #resolveContentStore(tenantId: string): Promise<ContentStore> {\n if (this.#contentStore) return this.#contentStore;\n return ContentStore.open(join(this.#root, \"content\"), { tenantId });\n }\n}\n\nasync function readSource(source: DocumentSource): Promise<string> {\n return source.type === \"inline\" ? source.content : readFile(source.path, \"utf8\");\n}\n\nfunction mimeFromPath(path: string): string {\n const extension = extname(path).toLowerCase();\n if (extension === \".md\" || extension === \".mdx\") return \"text/markdown\";\n if (extension === \".html\" || extension === \".htm\") return \"text/html\";\n if (extension === \".txt\" || extension === \".csv\" || extension === \".json\") return \"text/plain\";\n if (extension === \".pdf\") return \"application/pdf\";\n if (extension === \".docx\")\n return \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\";\n if (extension === \".pptx\")\n return \"application/vnd.openxmlformats-officedocument.presentationml.presentation\";\n if (extension === \".png\" || extension === \".jpg\" || extension === \".jpeg\") return \"image\";\n return \"application/octet-stream\";\n}\n\nfunction parseNativeDocument(\n request: RequiredTenant<ParseRequest>,\n raw: string,\n parser: ParserChoice,\n): ParsedDocument {\n const mimeType = request.source.mimeType ?? mimeFromPath(request.source.path);\n const { frontmatter, body } =\n parser === \"markdown\" ? parseFrontmatter(raw) : { frontmatter: {}, body: raw };\n const text = parser === \"html\" ? htmlToText(body) : body;\n const chunks = chunkParagraphs(text).map((chunk, index) => ({\n id: `${request.tenantId}:${request.source.path}:${index}`,\n text: chunk,\n metadata: {\n tenantId: request.tenantId,\n sourcePath: request.source.path,\n chunkIndex: index,\n mimeType,\n parser,\n ...(request.metadata ?? {}),\n },\n }));\n return {\n tenantId: request.tenantId,\n path: request.source.path,\n mimeType,\n parser,\n chunks:\n chunks.length > 0\n ? chunks\n : [\n {\n id: `${request.tenantId}:${request.source.path}:0`,\n text,\n metadata: {\n tenantId: request.tenantId,\n sourcePath: request.source.path,\n chunkIndex: 0,\n mimeType,\n parser,\n ...(request.metadata ?? {}),\n },\n },\n ],\n frontmatter,\n };\n}\n\nfunction serializeForContentStore(\n parsed: ParsedDocument,\n metadata: Record<string, string> | undefined,\n): string {\n const frontmatter = {\n ...parsed.frontmatter,\n ...(metadata ?? {}),\n source_mime: parsed.mimeType,\n parser: parsed.parser,\n };\n const frontmatterLines = Object.entries(frontmatter)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, value]) => `${key}: ${value}`);\n const body = parsed.chunks.map((chunk) => chunk.text).join(\"\\n\\n\");\n return frontmatterLines.length > 0 ? `---\\n${frontmatterLines.join(\"\\n\")}\\n---\\n${body}` : body;\n}\n\nfunction parseFrontmatter(content: string): {\n readonly frontmatter: Record<string, string>;\n readonly body: string;\n} {\n if (!content.startsWith(\"---\\n\")) return { frontmatter: {}, body: content };\n const end = content.indexOf(\"\\n---\", 4);\n if (end < 0) return { frontmatter: {}, body: content };\n const raw = content.slice(4, end).trim();\n const frontmatter: Record<string, string> = {};\n for (const line of raw.split(\"\\n\")) {\n const idx = line.indexOf(\":\");\n if (idx > 0) frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();\n }\n return { frontmatter, body: content.slice(end + 4).trimStart() };\n}\n\nfunction chunkParagraphs(content: string): string[] {\n return content\n .split(/\\n\\s*\\n/)\n .map((part) => part.trim())\n .filter(Boolean);\n}\n\nfunction htmlToText(html: string): string {\n return html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \" \")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \" \")\n .replace(/<\\/(h[1-6]|p|li|tr|div)>/gi, \"\\n\\n\")\n .replace(/<[^>]+>/g, \" \")\n .replace(/&nbsp;/g, \" \")\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/[ \\t]+/g, \" \")\n .replace(/\\n\\s+/g, \"\\n\")\n .trim();\n}\n"],"mappings":";AAAA,SAAS,OAAO,UAAU,iBAAiB;AAC3C,SAAS,SAAS,SAAS,YAAY;AACvC,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AA4FhC,SAAS,UAAU,OAAO,QAAQ,IAAI,GAAW;AAC/C,SAAO,KAAK,MAAM,YAAY,SAAS,yBAAyB;AAClE;AAEA,eAAe,YAAY,MAAc,OAA+C;AACtF,QAAM,OAAO,UAAU,IAAI;AAC3B,QAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9C,QAAM,UAAU,MAAM,GAAG,KAAK,UAAU,EAAE,KAAI,oBAAI,KAAK,GAAE,YAAY,GAAG,GAAG,MAAM,CAAC,CAAC;AAAA,GAAM;AAAA,IACvF,MAAM;AAAA,EACR,CAAC;AACH;AAEA,eAAsB,0BACpB,OAAO,QAAQ,IAAI,GACnB,QAAQ,IACY;AACpB,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,UAAU,IAAI,GAAG,MAAM;AAClD,WAAO,IACJ,KAAK,EACL,MAAM,IAAI,EACV,OAAO,OAAO,EACd,MAAM,CAAC,KAAK,EACZ,IAAI,CAAC,SAAS,KAAK,MAAM,IAAI,CAAY;AAAA,EAC9C,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,cAAc,UAA8B,UAAsC;AACzF,QAAM,WAAW,YAAY;AAC7B,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,gBAAgB,qBAAqB,8CAA8C;AAAA,MAC3F,YACE;AAAA,MACF,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEO,IAAM,mBAAN,MAAM,kBAAiB;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,UAAmC,CAAC,GAAG;AACjD,SAAK,YAAY,QAAQ;AACzB,SAAK,QAAQ,QAAQ,QAAQ,KAAK,QAAQ,IAAI,GAAG,YAAY,mBAAmB;AAChF,SAAK,aAAa,QAAQ,aAAa,QAAQ,IAAI;AACnD,SAAK,gBAAgB,QAAQ;AAC7B,SAAK,iBAAiB,QAAQ;AAAA,EAChC;AAAA,EAEA,aAAa,KACX,OAAO,8BACP,UAAkE,CAAC,GACxC;AAC3B,UAAM,WAAW,QAAQ,YAAY;AACrC,UAAM,eAAe,MAAM,aAAa,KAAK,KAAK,MAAM,SAAS,GAAG,EAAE,SAAS,CAAC;AAChF,WAAO,IAAI,kBAAiB,EAAE,GAAG,SAAS,UAAU,MAAM,aAAa,CAAC;AAAA,EAC1E;AAAA,EAEA,MAAM,MAAM,SAAgD;AAC1D,UAAM,WAAW,cAAc,QAAQ,UAAU,KAAK,SAAS;AAC/D,UAAM,WAAyC,EAAE,GAAG,SAAS,SAAS;AACtE,UAAM,SAAS,KAAK,cAAc,QAAQ;AAC1C,QAAI,WAAW,WAAW;AACxB,UAAI,CAAC,KAAK,gBAAgB;AACxB,cAAM,IAAI,gBAAgB,qBAAqB,oCAAoC;AAAA,UACjF,YACE;AAAA,UACF,YAAY;AAAA,UACZ,UAAU,EAAE,MAAM,QAAQ,OAAO,KAAK;AAAA,QACxC,CAAC;AAAA,MACH;AACA,YAAMA,UAAS,MAAM,KAAK,eAAe,MAAM,QAAQ;AACvD,YAAM,YAAY,KAAK,YAAY;AAAA,QACjC,MAAM;AAAA,QACN;AAAA,QACA,QAAQ;AAAA,QACR,MAAMA,QAAO;AAAA,MACf,CAAC;AACD,aAAOA;AAAA,IACT;AAEA,UAAM,MAAM,MAAM,WAAW,QAAQ,MAAM;AAC3C,UAAM,SAAS,oBAAoB,UAAU,KAAK,MAAM;AACxD,UAAM,YAAY,KAAK,YAAY;AAAA,MACjC,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,MAAM,OAAO;AAAA,MACb,QAAQ,OAAO,OAAO;AAAA,IACxB,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,OAAO,SAA+C;AAC1D,UAAM,WAAW,cAAc,QAAQ,UAAU,KAAK,SAAS;AAC/D,UAAM,eAAe,MAAM,KAAK,qBAAqB,QAAQ;AAC7D,UAAM,SAAS,MAAM,KAAK,MAAM,EAAE,GAAG,SAAS,SAAS,CAAC;AACxD,UAAM,aAAa,QAAQ,cAAc,OAAO;AAChD,UAAM,UAAU,yBAAyB,QAAQ,QAAQ,QAAQ;AACjE,UAAM,aAAa,MAAM,YAAY,OAAO;AAC5C,UAAM,YAAY,KAAK,YAAY;AAAA,MACjC,MAAM;AAAA,MACN;AAAA,MACA,MAAM;AAAA,MACN,QAAQ,OAAO;AAAA,MACf,QAAQ,OAAO,OAAO;AAAA,IACxB,CAAC;AACD,WAAO;AAAA,MACL;AAAA,MACA,MAAM;AAAA,MACN,QAAQ,OAAO;AAAA,MACf,YAAY,OAAO,OAAO;AAAA,MAC1B,gBAAgB;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,MAAM,WACJ,MACA,UAAyC,CAAC,GACnB;AACvB,WAAO,KAAK,OAAO,EAAE,GAAG,SAAS,QAAQ,EAAE,MAAM,QAAQ,KAAK,EAAE,CAAC;AAAA,EACnE;AAAA,EAEA,MAAM,SAAgD;AACpD,UAAM,eAAe,MAAM,KAAK,qBAAqB,KAAK,aAAa,OAAO;AAC9E,UAAM,UAAU,KAAK,iBAAiB,MAAM,KAAK,eAAe,OAAO,IAAI,EAAE,IAAI,MAAM;AACvF,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,eAAe,CAAC,YAAY,QAAQ,MAAM;AAAA,MAC1C,mBAAmB,QAAQ;AAAA,MAC3B,cAAc,MAAM,aAAa,OAAO;AAAA,MACxC,GAAI,QAAQ,KACR,CAAC,IACD;AAAA,QACE,YACE;AAAA,MACJ;AAAA,IACN;AAAA,EACF;AAAA,EAEA,cAAc,SAAqD;AACjE,QAAI,QAAQ,UAAU,QAAQ,WAAW,OAAQ,QAAO,QAAQ;AAChE,UAAM,OAAO,QAAQ,OAAO,YAAY,aAAa,QAAQ,OAAO,IAAI;AACxE,QAAI,SAAS,gBAAiB,QAAO;AACrC,QAAI,SAAS,YAAa,QAAO;AACjC,QAAI,KAAK,WAAW,OAAO,EAAG,QAAO;AACrC,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,qBAAqB,UAAyC;AAClE,QAAI,KAAK,cAAe,QAAO,KAAK;AACpC,WAAO,aAAa,KAAK,KAAK,KAAK,OAAO,SAAS,GAAG,EAAE,SAAS,CAAC;AAAA,EACpE;AACF;AAEA,eAAe,WAAW,QAAyC;AACjE,SAAO,OAAO,SAAS,WAAW,OAAO,UAAU,SAAS,OAAO,MAAM,MAAM;AACjF;AAEA,SAAS,aAAa,MAAsB;AAC1C,QAAM,YAAY,QAAQ,IAAI,EAAE,YAAY;AAC5C,MAAI,cAAc,SAAS,cAAc,OAAQ,QAAO;AACxD,MAAI,cAAc,WAAW,cAAc,OAAQ,QAAO;AAC1D,MAAI,cAAc,UAAU,cAAc,UAAU,cAAc,QAAS,QAAO;AAClF,MAAI,cAAc,OAAQ,QAAO;AACjC,MAAI,cAAc;AAChB,WAAO;AACT,MAAI,cAAc;AAChB,WAAO;AACT,MAAI,cAAc,UAAU,cAAc,UAAU,cAAc,QAAS,QAAO;AAClF,SAAO;AACT;AAEA,SAAS,oBACP,SACA,KACA,QACgB;AAChB,QAAM,WAAW,QAAQ,OAAO,YAAY,aAAa,QAAQ,OAAO,IAAI;AAC5E,QAAM,EAAE,aAAa,KAAK,IACxB,WAAW,aAAa,iBAAiB,GAAG,IAAI,EAAE,aAAa,CAAC,GAAG,MAAM,IAAI;AAC/E,QAAM,OAAO,WAAW,SAAS,WAAW,IAAI,IAAI;AACpD,QAAM,SAAS,gBAAgB,IAAI,EAAE,IAAI,CAAC,OAAO,WAAW;AAAA,IAC1D,IAAI,GAAG,QAAQ,QAAQ,IAAI,QAAQ,OAAO,IAAI,IAAI,KAAK;AAAA,IACvD,MAAM;AAAA,IACN,UAAU;AAAA,MACR,UAAU,QAAQ;AAAA,MAClB,YAAY,QAAQ,OAAO;AAAA,MAC3B,YAAY;AAAA,MACZ;AAAA,MACA;AAAA,MACA,GAAI,QAAQ,YAAY,CAAC;AAAA,IAC3B;AAAA,EACF,EAAE;AACF,SAAO;AAAA,IACL,UAAU,QAAQ;AAAA,IAClB,MAAM,QAAQ,OAAO;AAAA,IACrB;AAAA,IACA;AAAA,IACA,QACE,OAAO,SAAS,IACZ,SACA;AAAA,MACE;AAAA,QACE,IAAI,GAAG,QAAQ,QAAQ,IAAI,QAAQ,OAAO,IAAI;AAAA,QAC9C;AAAA,QACA,UAAU;AAAA,UACR,UAAU,QAAQ;AAAA,UAClB,YAAY,QAAQ,OAAO;AAAA,UAC3B,YAAY;AAAA,UACZ;AAAA,UACA;AAAA,UACA,GAAI,QAAQ,YAAY,CAAC;AAAA,QAC3B;AAAA,MACF;AAAA,IACF;AAAA,IACN;AAAA,EACF;AACF;AAEA,SAAS,yBACP,QACA,UACQ;AACR,QAAM,cAAc;AAAA,IAClB,GAAG,OAAO;AAAA,IACV,GAAI,YAAY,CAAC;AAAA,IACjB,aAAa,OAAO;AAAA,IACpB,QAAQ,OAAO;AAAA,EACjB;AACA,QAAM,mBAAmB,OAAO,QAAQ,WAAW,EAChD,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,MAAM,KAAK,cAAc,KAAK,CAAC,EACnD,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,GAAG,GAAG,KAAK,KAAK,EAAE;AAC3C,QAAM,OAAO,OAAO,OAAO,IAAI,CAAC,UAAU,MAAM,IAAI,EAAE,KAAK,MAAM;AACjE,SAAO,iBAAiB,SAAS,IAAI;AAAA,EAAQ,iBAAiB,KAAK,IAAI,CAAC;AAAA;AAAA,EAAU,IAAI,KAAK;AAC7F;AAEA,SAAS,iBAAiB,SAGxB;AACA,MAAI,CAAC,QAAQ,WAAW,OAAO,EAAG,QAAO,EAAE,aAAa,CAAC,GAAG,MAAM,QAAQ;AAC1E,QAAM,MAAM,QAAQ,QAAQ,SAAS,CAAC;AACtC,MAAI,MAAM,EAAG,QAAO,EAAE,aAAa,CAAC,GAAG,MAAM,QAAQ;AACrD,QAAM,MAAM,QAAQ,MAAM,GAAG,GAAG,EAAE,KAAK;AACvC,QAAM,cAAsC,CAAC;AAC7C,aAAW,QAAQ,IAAI,MAAM,IAAI,GAAG;AAClC,UAAM,MAAM,KAAK,QAAQ,GAAG;AAC5B,QAAI,MAAM,EAAG,aAAY,KAAK,MAAM,GAAG,GAAG,EAAE,KAAK,CAAC,IAAI,KAAK,MAAM,MAAM,CAAC,EAAE,KAAK;AAAA,EACjF;AACA,SAAO,EAAE,aAAa,MAAM,QAAQ,MAAM,MAAM,CAAC,EAAE,UAAU,EAAE;AACjE;AAEA,SAAS,gBAAgB,SAA2B;AAClD,SAAO,QACJ,MAAM,SAAS,EACf,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EACzB,OAAO,OAAO;AACnB;AAEA,SAAS,WAAW,MAAsB;AACxC,SAAO,KACJ,QAAQ,+BAA+B,GAAG,EAC1C,QAAQ,6BAA6B,GAAG,EACxC,QAAQ,8BAA8B,MAAM,EAC5C,QAAQ,YAAY,GAAG,EACvB,QAAQ,WAAW,GAAG,EACtB,QAAQ,UAAU,GAAG,EACrB,QAAQ,SAAS,GAAG,EACpB,QAAQ,SAAS,GAAG,EACpB,QAAQ,WAAW,GAAG,EACtB,QAAQ,UAAU,IAAI,EACtB,KAAK;AACV;","names":["parsed"]}