@gmickel/gno 1.27.0 → 1.28.0
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/README.md +2 -2
- package/assets/skill/SKILL.md +26 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v1.27.0.zip → gno-browser-clipper-v1.28.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/spec/cli.md +41 -4
- package/spec/db/schema.sql +12 -0
- package/spec/mcp.md +5 -0
- package/spec/output-schemas/ask.schema.json +125 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +94 -1
- package/spec/output-schemas/get.schema.json +94 -0
- package/spec/output-schemas/mcp-job-status.schema.json +28 -0
- package/spec/output-schemas/multi-get.schema.json +128 -0
- package/spec/output-schemas/record-import.schema.json +193 -0
- package/spec/output-schemas/search-result.schema.json +94 -0
- package/spec/output-schemas/search-results.schema.json +125 -0
- package/spec/project-profile.schema.json +66 -0
- package/src/app/context-format.ts +1 -1
- package/src/cli/commands/get.ts +12 -2
- package/src/cli/commands/index-cmd.ts +13 -0
- package/src/cli/commands/multi-get.ts +9 -2
- package/src/cli/commands/shared.ts +28 -0
- package/src/cli/commands/update.ts +5 -0
- package/src/cli/program.ts +4 -0
- package/src/config/project-profile.ts +2 -0
- package/src/config/types.ts +16 -0
- package/src/converters/adapters/browser-export/adapter.ts +199 -0
- package/src/converters/adapters/browser-export/formats.ts +358 -0
- package/src/converters/adapters/email/adapter.ts +429 -0
- package/src/converters/adapters/email/html.ts +162 -0
- package/src/converters/adapters/email/mime.ts +454 -0
- package/src/converters/adapters/email/parameters.ts +77 -0
- package/src/converters/adapters/ical/adapter.ts +475 -0
- package/src/converters/adapters/ical/recurrence.ts +186 -0
- package/src/converters/adapters/jsonl/adapter.ts +238 -0
- package/src/converters/adapters/jsonl/config.ts +105 -0
- package/src/converters/adapters/shared/html-text.ts +165 -0
- package/src/converters/adapters/shared/record-utils.ts +79 -0
- package/src/converters/adapters/shared/utf8-lines.ts +141 -0
- package/src/converters/adapters/transcript/adapter.ts +295 -0
- package/src/converters/adapters/transcript/json.ts +184 -0
- package/src/converters/adapters/transcript/model.ts +171 -0
- package/src/converters/adapters/transcript/text.ts +58 -0
- package/src/converters/adapters/transcript/timed.ts +152 -0
- package/src/converters/index.ts +11 -1
- package/src/converters/mime.ts +8 -0
- package/src/converters/pipeline.ts +15 -1
- package/src/converters/registry.ts +37 -1
- package/src/converters/types.ts +136 -0
- package/src/core/context-capsule-schema.ts +87 -0
- package/src/core/context-capsule.ts +20 -0
- package/src/core/context-evidence.ts +7 -1
- package/src/core/document-capabilities.ts +12 -0
- package/src/core/project-profile-apply-state.ts +5 -0
- package/src/core/project-profile.ts +4 -0
- package/src/core/record-metadata.ts +49 -0
- package/src/ingestion/record-adapter-canonical.ts +433 -0
- package/src/ingestion/record-adapter.ts +437 -0
- package/src/ingestion/record-container.ts +688 -0
- package/src/ingestion/record-path.ts +20 -0
- package/src/ingestion/record-sync.ts +70 -0
- package/src/ingestion/sync.ts +228 -36
- package/src/ingestion/types.ts +69 -1
- package/src/ingestion/walker.ts +31 -11
- package/src/mcp/tools/get.ts +10 -2
- package/src/mcp/tools/multi-get.ts +9 -2
- package/src/mcp/tools/workspace-write.ts +2 -0
- package/src/pipeline/filters.ts +1 -1
- package/src/pipeline/graph-retrieval.ts +7 -4
- package/src/pipeline/hybrid.ts +7 -4
- package/src/pipeline/result-context.ts +12 -4
- package/src/pipeline/search.ts +11 -4
- package/src/pipeline/types.ts +3 -0
- package/src/pipeline/vsearch.ts +26 -8
- package/src/sdk/documents.ts +13 -5
- package/src/serve/browse-tree.ts +4 -2
- package/src/serve/routes/api.ts +63 -65
- package/src/store/migrations/022-record-export-lineage.ts +42 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +114 -7
- package/src/store/types.ts +40 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.0.zip.sha256 +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeHtmlEntitiesOnce,
|
|
3
|
+
htmlFragmentToText,
|
|
4
|
+
} from "../shared/html-text";
|
|
5
|
+
import {
|
|
6
|
+
safeInlineText,
|
|
7
|
+
safeMarkdownText,
|
|
8
|
+
scalarText,
|
|
9
|
+
} from "../shared/record-utils";
|
|
10
|
+
|
|
11
|
+
export type TranscriptFormat = "auto" | "json" | "srt" | "text" | "vtt";
|
|
12
|
+
|
|
13
|
+
export interface TranscriptAdapterOptions {
|
|
14
|
+
/** Generic JSON and text sources require explicit selection at registration. */
|
|
15
|
+
format?: TranscriptFormat;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TranscriptSegment {
|
|
19
|
+
externalId?: string;
|
|
20
|
+
text: string;
|
|
21
|
+
speaker?: string;
|
|
22
|
+
start?: string;
|
|
23
|
+
end?: string;
|
|
24
|
+
sourceLocator: string;
|
|
25
|
+
anchorKind: "cue" | "line" | "record";
|
|
26
|
+
anchorValue: string;
|
|
27
|
+
endAnchorValue?: string;
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
sessionTitle?: string;
|
|
30
|
+
participants?: string[];
|
|
31
|
+
dateFields?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type TranscriptParseEvent =
|
|
35
|
+
| { ok: true; segment: TranscriptSegment }
|
|
36
|
+
| {
|
|
37
|
+
ok: false;
|
|
38
|
+
sourceLocator?: string;
|
|
39
|
+
retryable: boolean;
|
|
40
|
+
tooLarge?: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const TIMESTAMP_PATTERN = /^(?:(\d{1,}):)?([0-5]?\d):([0-5]\d)[,.](\d{3})$/;
|
|
44
|
+
|
|
45
|
+
export const parseTranscriptTimestamp = (
|
|
46
|
+
value: unknown
|
|
47
|
+
): { text: string; milliseconds: number } | undefined => {
|
|
48
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
49
|
+
const milliseconds = Math.round(value * 1_000);
|
|
50
|
+
if (Number.isSafeInteger(milliseconds)) {
|
|
51
|
+
return { text: formatTranscriptTimestamp(milliseconds), milliseconds };
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const scalar = scalarText(value);
|
|
56
|
+
if (!scalar) return undefined;
|
|
57
|
+
if (/^\d+(?:\.\d+)?$/.test(scalar)) {
|
|
58
|
+
const milliseconds = Math.round(Number(scalar) * 1_000);
|
|
59
|
+
if (Number.isSafeInteger(milliseconds)) {
|
|
60
|
+
return { text: formatTranscriptTimestamp(milliseconds), milliseconds };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const match = TIMESTAMP_PATTERN.exec(scalar);
|
|
64
|
+
if (!match) return undefined;
|
|
65
|
+
const hours = Number(match[1] ?? "0");
|
|
66
|
+
const minutes = Number(match[2]);
|
|
67
|
+
const seconds = Number(match[3]);
|
|
68
|
+
const millisecondsPart = Number(match[4]);
|
|
69
|
+
const milliseconds =
|
|
70
|
+
(hours * 60 * 60 + minutes * 60 + seconds) * 1_000 + millisecondsPart;
|
|
71
|
+
if (!Number.isSafeInteger(milliseconds)) return undefined;
|
|
72
|
+
return { text: formatTranscriptTimestamp(milliseconds), milliseconds };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const formatTranscriptTimestamp = (milliseconds: number): string => {
|
|
76
|
+
const totalSeconds = Math.floor(milliseconds / 1_000);
|
|
77
|
+
const hours = Math.floor(totalSeconds / 3_600);
|
|
78
|
+
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
79
|
+
const seconds = totalSeconds % 60;
|
|
80
|
+
const millis = milliseconds % 1_000;
|
|
81
|
+
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(millis).padStart(3, "0")}`;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const VALID_TRANSCRIPT_FORMATS = new Set<TranscriptFormat>([
|
|
85
|
+
"auto",
|
|
86
|
+
"json",
|
|
87
|
+
"srt",
|
|
88
|
+
"text",
|
|
89
|
+
"vtt",
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
export const parseTranscriptAdapterOptions = (
|
|
93
|
+
options: TranscriptAdapterOptions
|
|
94
|
+
): Required<TranscriptAdapterOptions> => {
|
|
95
|
+
const format = options.format ?? "auto";
|
|
96
|
+
if (!VALID_TRANSCRIPT_FORMATS.has(format)) {
|
|
97
|
+
throw new Error("Unsupported transcript adapter format.");
|
|
98
|
+
}
|
|
99
|
+
return { format };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const vttVoiceSpeaker = (value: string): string | undefined => {
|
|
103
|
+
const source = value.trimStart();
|
|
104
|
+
if (
|
|
105
|
+
source.length < 3 ||
|
|
106
|
+
source[0] !== "<" ||
|
|
107
|
+
source[1]?.toLowerCase() !== "v"
|
|
108
|
+
)
|
|
109
|
+
return undefined;
|
|
110
|
+
const end = source.indexOf(">", 2);
|
|
111
|
+
if (end < 0) return undefined;
|
|
112
|
+
let cursor = 2;
|
|
113
|
+
if (source[cursor] === ".") {
|
|
114
|
+
while (
|
|
115
|
+
cursor < end &&
|
|
116
|
+
source[cursor] !== " " &&
|
|
117
|
+
source[cursor] !== "\t" &&
|
|
118
|
+
source[cursor] !== "\n" &&
|
|
119
|
+
source[cursor] !== "\r"
|
|
120
|
+
)
|
|
121
|
+
cursor += 1;
|
|
122
|
+
}
|
|
123
|
+
if (
|
|
124
|
+
source[cursor] !== " " &&
|
|
125
|
+
source[cursor] !== "\t" &&
|
|
126
|
+
source[cursor] !== "\n" &&
|
|
127
|
+
source[cursor] !== "\r"
|
|
128
|
+
)
|
|
129
|
+
return undefined;
|
|
130
|
+
while (
|
|
131
|
+
cursor < end &&
|
|
132
|
+
(source[cursor] === " " ||
|
|
133
|
+
source[cursor] === "\t" ||
|
|
134
|
+
source[cursor] === "\n" ||
|
|
135
|
+
source[cursor] === "\r")
|
|
136
|
+
)
|
|
137
|
+
cursor += 1;
|
|
138
|
+
const speaker = source.slice(cursor, end).trim();
|
|
139
|
+
if (!speaker || speaker.length > 80) return undefined;
|
|
140
|
+
return safeInlineText(decodeHtmlEntitiesOnce(speaker));
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const speakerPrefix = (
|
|
144
|
+
value: string
|
|
145
|
+
): { speaker: string; text: string } | undefined => {
|
|
146
|
+
const separator = value.indexOf(":");
|
|
147
|
+
if (separator <= 0 || separator > 80) return undefined;
|
|
148
|
+
const speaker = value.slice(0, separator);
|
|
149
|
+
if (speaker.includes("\n")) return undefined;
|
|
150
|
+
const remainder = value.slice(separator + 1);
|
|
151
|
+
if (
|
|
152
|
+
remainder.length === 0 ||
|
|
153
|
+
(remainder[0] !== " " &&
|
|
154
|
+
remainder[0] !== "\t" &&
|
|
155
|
+
remainder[0] !== "\n" &&
|
|
156
|
+
remainder[0] !== "\r")
|
|
157
|
+
)
|
|
158
|
+
return undefined;
|
|
159
|
+
const text = remainder.trim();
|
|
160
|
+
if (!text) return undefined;
|
|
161
|
+
return { speaker: safeInlineText(speaker), text };
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const cleanTranscriptText = (
|
|
165
|
+
value: string
|
|
166
|
+
): { text: string; speaker?: string } => {
|
|
167
|
+
const speaker = vttVoiceSpeaker(value);
|
|
168
|
+
const cleaned = safeMarkdownText(htmlFragmentToText(value)).trim();
|
|
169
|
+
if (speaker) return { text: cleaned, speaker };
|
|
170
|
+
return speakerPrefix(cleaned) ?? { text: cleaned };
|
|
171
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Utf8LineResult } from "../shared/utf8-lines";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cleanTranscriptText,
|
|
5
|
+
parseTranscriptTimestamp,
|
|
6
|
+
type TranscriptParseEvent,
|
|
7
|
+
type TranscriptSegment,
|
|
8
|
+
} from "./model";
|
|
9
|
+
|
|
10
|
+
const TIMESTAMP_PREFIX = /^\[([^\]]{1,32})\]\s*(.*)$/su;
|
|
11
|
+
|
|
12
|
+
export async function* parseTextTranscript(
|
|
13
|
+
source: AsyncIterable<Utf8LineResult>
|
|
14
|
+
): AsyncGenerator<TranscriptParseEvent> {
|
|
15
|
+
for await (const line of source) {
|
|
16
|
+
if (!line.ok) {
|
|
17
|
+
yield {
|
|
18
|
+
ok: false,
|
|
19
|
+
sourceLocator: `line:${line.lineNumber}`,
|
|
20
|
+
retryable: !line.terminated,
|
|
21
|
+
tooLarge: line.reason === "line_too_large",
|
|
22
|
+
};
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const raw = line.text.trim();
|
|
26
|
+
if (!raw) continue;
|
|
27
|
+
const timestampPrefix = TIMESTAMP_PREFIX.exec(raw);
|
|
28
|
+
const timestamp = timestampPrefix
|
|
29
|
+
? parseTranscriptTimestamp(timestampPrefix[1])
|
|
30
|
+
: undefined;
|
|
31
|
+
if (timestampPrefix && !timestamp) {
|
|
32
|
+
yield {
|
|
33
|
+
ok: false,
|
|
34
|
+
sourceLocator: `line:${line.lineNumber}`,
|
|
35
|
+
retryable: false,
|
|
36
|
+
};
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const cleaned = cleanTranscriptText(timestampPrefix?.[2] ?? raw);
|
|
40
|
+
if (!cleaned.text) {
|
|
41
|
+
yield {
|
|
42
|
+
ok: false,
|
|
43
|
+
sourceLocator: `line:${line.lineNumber}`,
|
|
44
|
+
retryable: false,
|
|
45
|
+
};
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const segment: TranscriptSegment = {
|
|
49
|
+
text: cleaned.text,
|
|
50
|
+
speaker: cleaned.speaker,
|
|
51
|
+
start: timestamp?.text,
|
|
52
|
+
sourceLocator: `line:${line.lineNumber}`,
|
|
53
|
+
anchorKind: "line",
|
|
54
|
+
anchorValue: String(line.lineNumber),
|
|
55
|
+
};
|
|
56
|
+
yield { ok: true, segment };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Utf8LineResult } from "../shared/utf8-lines";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cleanTranscriptText,
|
|
5
|
+
parseTranscriptTimestamp,
|
|
6
|
+
type TranscriptParseEvent,
|
|
7
|
+
type TranscriptSegment,
|
|
8
|
+
} from "./model";
|
|
9
|
+
|
|
10
|
+
interface SourceLine {
|
|
11
|
+
lineNumber: number;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const VTT_TIMING_PATTERN =
|
|
16
|
+
/^(\d{1,}:\d{2}:\d{2}\.\d{3}|\d{1,2}:\d{2}\.\d{3})\s+-->\s+(\d{1,}:\d{2}:\d{2}\.\d{3}|\d{1,2}:\d{2}\.\d{3})(?:\s+.*)?$/u;
|
|
17
|
+
const SRT_TIMING_PATTERN =
|
|
18
|
+
/^(\d{1,}:\d{2}:\d{2}[,.]\d{3})\s+-->\s+(\d{1,}:\d{2}:\d{2}[,.]\d{3})(?:\s+.*)?$/u;
|
|
19
|
+
|
|
20
|
+
const locator = (lines: readonly SourceLine[]): string => {
|
|
21
|
+
const first = lines[0]?.lineNumber ?? 1;
|
|
22
|
+
const last = lines.at(-1)?.lineNumber ?? first;
|
|
23
|
+
return `lines:${first}-${last}`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const malformed = (
|
|
27
|
+
lines: readonly SourceLine[],
|
|
28
|
+
retryable = false
|
|
29
|
+
): TranscriptParseEvent => ({
|
|
30
|
+
ok: false,
|
|
31
|
+
sourceLocator: locator(lines),
|
|
32
|
+
retryable,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const parseCueBlock = (
|
|
36
|
+
lines: readonly SourceLine[],
|
|
37
|
+
format: "srt" | "vtt"
|
|
38
|
+
): TranscriptParseEvent | undefined => {
|
|
39
|
+
if (lines.length === 0) return undefined;
|
|
40
|
+
const first = lines[0]?.text.trim() ?? "";
|
|
41
|
+
if (format === "vtt" && /^(?:NOTE|STYLE|REGION)(?:\s|$)/u.test(first)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const timingPattern =
|
|
46
|
+
format === "vtt" ? VTT_TIMING_PATTERN : SRT_TIMING_PATTERN;
|
|
47
|
+
let timingIndex = 0;
|
|
48
|
+
let externalId: string | undefined;
|
|
49
|
+
if (!timingPattern.test(first)) {
|
|
50
|
+
if (!lines[1] || !timingPattern.test(lines[1].text.trim())) {
|
|
51
|
+
return malformed(lines);
|
|
52
|
+
}
|
|
53
|
+
externalId = first || undefined;
|
|
54
|
+
timingIndex = 1;
|
|
55
|
+
}
|
|
56
|
+
const timing = timingPattern.exec(lines[timingIndex]?.text.trim() ?? "");
|
|
57
|
+
if (!(timing?.[1] && timing[2])) return malformed(lines);
|
|
58
|
+
const start = parseTranscriptTimestamp(timing[1]);
|
|
59
|
+
const end = parseTranscriptTimestamp(timing[2]);
|
|
60
|
+
if (!(start && end) || end.milliseconds < start.milliseconds) {
|
|
61
|
+
return malformed(lines);
|
|
62
|
+
}
|
|
63
|
+
const payload = lines
|
|
64
|
+
.slice(timingIndex + 1)
|
|
65
|
+
.map((line) => line.text)
|
|
66
|
+
.join("\n");
|
|
67
|
+
const cleaned = cleanTranscriptText(payload);
|
|
68
|
+
if (!cleaned.text) return malformed(lines);
|
|
69
|
+
const firstLine = lines[0]?.lineNumber ?? 1;
|
|
70
|
+
const lastLine = lines.at(-1)?.lineNumber ?? firstLine;
|
|
71
|
+
const segment: TranscriptSegment = {
|
|
72
|
+
externalId,
|
|
73
|
+
text: cleaned.text,
|
|
74
|
+
speaker: cleaned.speaker,
|
|
75
|
+
start: start.text,
|
|
76
|
+
end: end.text,
|
|
77
|
+
sourceLocator: `lines:${firstLine}-${lastLine}`,
|
|
78
|
+
anchorKind: "cue",
|
|
79
|
+
anchorValue: externalId ?? start.text,
|
|
80
|
+
endAnchorValue: end.text,
|
|
81
|
+
};
|
|
82
|
+
return { ok: true, segment };
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export async function* parseTimedTranscript(
|
|
86
|
+
source: AsyncIterable<Utf8LineResult>,
|
|
87
|
+
format: "srt" | "vtt"
|
|
88
|
+
): AsyncGenerator<TranscriptParseEvent> {
|
|
89
|
+
let block: SourceLine[] = [];
|
|
90
|
+
let vttHeaderChecked = format !== "vtt";
|
|
91
|
+
let vttHeaderEnded = format !== "vtt";
|
|
92
|
+
let headerInvalid = false;
|
|
93
|
+
let lastLineNumber = 0;
|
|
94
|
+
|
|
95
|
+
const flush = (): TranscriptParseEvent | undefined => {
|
|
96
|
+
const event = parseCueBlock(block, format);
|
|
97
|
+
block = [];
|
|
98
|
+
return event;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
for await (const line of source) {
|
|
102
|
+
lastLineNumber = line.lineNumber;
|
|
103
|
+
if (!line.ok) {
|
|
104
|
+
const pending = flush();
|
|
105
|
+
if (pending) yield pending;
|
|
106
|
+
yield {
|
|
107
|
+
ok: false,
|
|
108
|
+
sourceLocator: `line:${line.lineNumber}`,
|
|
109
|
+
retryable: !line.terminated,
|
|
110
|
+
tooLarge: line.reason === "line_too_large",
|
|
111
|
+
};
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!vttHeaderChecked) {
|
|
116
|
+
vttHeaderChecked = true;
|
|
117
|
+
if (!line.text.startsWith("WEBVTT")) {
|
|
118
|
+
headerInvalid = true;
|
|
119
|
+
yield {
|
|
120
|
+
ok: false,
|
|
121
|
+
sourceLocator: `line:${line.lineNumber}`,
|
|
122
|
+
retryable: false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (headerInvalid) continue;
|
|
128
|
+
if (!vttHeaderEnded) {
|
|
129
|
+
if (line.text.trim() === "") vttHeaderEnded = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (line.text.trim() === "") {
|
|
134
|
+
const event = flush();
|
|
135
|
+
if (event) yield event;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
block.push({ lineNumber: line.lineNumber, text: line.text });
|
|
139
|
+
}
|
|
140
|
+
if (!headerInvalid && !vttHeaderEnded) {
|
|
141
|
+
yield {
|
|
142
|
+
ok: false,
|
|
143
|
+
sourceLocator: `lines:1-${Math.max(1, lastLineNumber)}`,
|
|
144
|
+
retryable: true,
|
|
145
|
+
};
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!headerInvalid) {
|
|
149
|
+
const event = flush();
|
|
150
|
+
if (event) yield event;
|
|
151
|
+
}
|
|
152
|
+
}
|
package/src/converters/index.ts
CHANGED
|
@@ -47,5 +47,15 @@ export type {
|
|
|
47
47
|
ConvertResult,
|
|
48
48
|
ConvertWarning,
|
|
49
49
|
PipelineResult,
|
|
50
|
+
RecordAdapter,
|
|
51
|
+
RecordAdapterEvent,
|
|
52
|
+
RecordAdapterFailure,
|
|
53
|
+
RecordAdapterFailureCode,
|
|
54
|
+
RecordAdapterInput,
|
|
55
|
+
RecordAdapterLimits,
|
|
56
|
+
RecordAdapterRecord,
|
|
57
|
+
RecordAnchor,
|
|
58
|
+
RecordAttachmentInventoryItem,
|
|
59
|
+
RecordMetadata,
|
|
50
60
|
} from "./types";
|
|
51
|
-
export { DEFAULT_LIMITS } from "./types";
|
|
61
|
+
export { DEFAULT_LIMITS, DEFAULT_RECORD_ADAPTER_LIMITS } from "./types";
|
package/src/converters/mime.ts
CHANGED
|
@@ -35,6 +35,14 @@ const EXTENSION_MAP: Record<string, string> = {
|
|
|
35
35
|
".pptx":
|
|
36
36
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
37
37
|
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
38
|
+
".jsonl": "application/x-ndjson",
|
|
39
|
+
".ndjson": "application/x-ndjson",
|
|
40
|
+
".eml": "message/rfc822",
|
|
41
|
+
".mbox": "application/mbox",
|
|
42
|
+
".ics": "text/calendar",
|
|
43
|
+
".vtt": "text/vtt",
|
|
44
|
+
".srt": "application/x-subrip",
|
|
45
|
+
".browser-export": "application/x-gno-browser-export+json",
|
|
38
46
|
};
|
|
39
47
|
|
|
40
48
|
/** OOXML extension to MIME mapping */
|
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
* CRITICAL: Canonicalization is ONLY done here, not in individual converters.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
ConversionArtifact,
|
|
18
|
+
ConvertInput,
|
|
19
|
+
PipelineResult,
|
|
20
|
+
RecordAdapter,
|
|
21
|
+
} from "./types";
|
|
17
22
|
|
|
18
23
|
import { canonicalize, mirrorHash } from "./canonicalize";
|
|
19
24
|
import { internalError, outputTooLargeError } from "./errors";
|
|
@@ -135,6 +140,15 @@ export class ConversionPipeline {
|
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
/** Select a streaming multi-record adapter without reading the full file. */
|
|
144
|
+
async selectRecordAdapter(
|
|
145
|
+
mime: string,
|
|
146
|
+
ext: string
|
|
147
|
+
): Promise<RecordAdapter | undefined> {
|
|
148
|
+
const registry = await this.ensureRegistry();
|
|
149
|
+
return registry.selectRecordAdapter(mime, ext);
|
|
150
|
+
}
|
|
151
|
+
|
|
138
152
|
/**
|
|
139
153
|
* List available converters.
|
|
140
154
|
*/
|
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
* PRD §8.6 - Converter registry
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
Converter,
|
|
8
|
+
ConvertInput,
|
|
9
|
+
ConvertResult,
|
|
10
|
+
RecordAdapter,
|
|
11
|
+
} from "./types";
|
|
7
12
|
|
|
8
13
|
import { unsupportedError } from "./errors";
|
|
9
14
|
|
|
10
15
|
export class ConverterRegistry {
|
|
11
16
|
private readonly converters: Converter[] = [];
|
|
17
|
+
private readonly recordAdapters: RecordAdapter[] = [];
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Register a converter. Order matters - first match wins.
|
|
@@ -34,6 +40,25 @@ export class ConverterRegistry {
|
|
|
34
40
|
return this.converters.map((c) => c.id);
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
/** Register a streaming container adapter without changing converter routing. */
|
|
44
|
+
registerRecordAdapter(adapter: RecordAdapter): void {
|
|
45
|
+
this.recordAdapters.push(adapter);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Select the first streaming adapter that handles a MIME/extension pair. */
|
|
49
|
+
selectRecordAdapter(mime: string, ext: string): RecordAdapter | undefined {
|
|
50
|
+
const normalizedMime = mime.toLowerCase();
|
|
51
|
+
const normalizedExt = ext.toLowerCase();
|
|
52
|
+
return this.recordAdapters.find((adapter) =>
|
|
53
|
+
adapter.canHandle(normalizedMime, normalizedExt)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** List streaming adapters independently of byte-oriented converters. */
|
|
58
|
+
listRecordAdapters(): string[] {
|
|
59
|
+
return this.recordAdapters.map((adapter) => adapter.id);
|
|
60
|
+
}
|
|
61
|
+
|
|
37
62
|
/**
|
|
38
63
|
* Convert a file using the appropriate converter.
|
|
39
64
|
*/
|
|
@@ -63,12 +88,23 @@ export async function createDefaultRegistry(): Promise<ConverterRegistry> {
|
|
|
63
88
|
const { markitdownAdapter } = await import("./adapters/markitdownTs/adapter");
|
|
64
89
|
const { officeparserAdapter } =
|
|
65
90
|
await import("./adapters/officeparser/adapter");
|
|
91
|
+
const { jsonlAdapter } = await import("./adapters/jsonl/adapter");
|
|
92
|
+
const { transcriptAdapter } = await import("./adapters/transcript/adapter");
|
|
93
|
+
const { emailRecordAdapter } = await import("./adapters/email/adapter");
|
|
94
|
+
const { icalAdapter } = await import("./adapters/ical/adapter");
|
|
95
|
+
const { browserExportAdapter } =
|
|
96
|
+
await import("./adapters/browser-export/adapter");
|
|
66
97
|
|
|
67
98
|
// Register in priority order
|
|
68
99
|
registry.register(markdownConverter);
|
|
69
100
|
registry.register(plaintextConverter);
|
|
70
101
|
registry.register(markitdownAdapter);
|
|
71
102
|
registry.register(officeparserAdapter);
|
|
103
|
+
registry.registerRecordAdapter(jsonlAdapter);
|
|
104
|
+
registry.registerRecordAdapter(transcriptAdapter);
|
|
105
|
+
registry.registerRecordAdapter(emailRecordAdapter);
|
|
106
|
+
registry.registerRecordAdapter(icalAdapter);
|
|
107
|
+
registry.registerRecordAdapter(browserExportAdapter);
|
|
72
108
|
|
|
73
109
|
return registry;
|
|
74
110
|
}
|
package/src/converters/types.ts
CHANGED
|
@@ -94,6 +94,132 @@ export interface Converter {
|
|
|
94
94
|
convert(input: ConvertInput): Promise<ConvertResult>;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/** Declarative limits applied to every multi-record container adapter. */
|
|
98
|
+
export interface RecordAdapterLimits {
|
|
99
|
+
/** End-to-end adapter deadline; production callers always set it. */
|
|
100
|
+
timeoutMs?: number;
|
|
101
|
+
/** Maximum bytes an adapter may read from the container source. */
|
|
102
|
+
maxSourceBytes: number;
|
|
103
|
+
/** Maximum canonical characters accepted for one logical record. */
|
|
104
|
+
maxRecordChars: number;
|
|
105
|
+
/** Maximum canonical metadata characters accepted for one logical record. */
|
|
106
|
+
maxMetadataChars: number;
|
|
107
|
+
/** Maximum canonical characters retained across the whole snapshot. */
|
|
108
|
+
maxTotalChars: number;
|
|
109
|
+
/** Maximum logical records retained from one snapshot. */
|
|
110
|
+
maxRecords: number;
|
|
111
|
+
/** Maximum isolated failures retained before consumption stops. */
|
|
112
|
+
maxFailures: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Input for a streaming adapter. The opener is wrapped with the source cap. */
|
|
116
|
+
export interface RecordAdapterInput {
|
|
117
|
+
sourcePath: string;
|
|
118
|
+
relativePath: string;
|
|
119
|
+
collection: string;
|
|
120
|
+
mime: string;
|
|
121
|
+
ext: string;
|
|
122
|
+
open: (signal?: AbortSignal) => AsyncIterable<Uint8Array>;
|
|
123
|
+
signal?: AbortSignal;
|
|
124
|
+
limits: RecordAdapterLimits;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface RecordAnchor {
|
|
128
|
+
kind: "line" | "cue" | "timestamp" | "message" | "event" | "record";
|
|
129
|
+
value: string;
|
|
130
|
+
endValue?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface RecordAttachmentInventoryItem {
|
|
134
|
+
name: string;
|
|
135
|
+
mime?: string;
|
|
136
|
+
bytes?: number;
|
|
137
|
+
disposition?: "inline" | "attachment";
|
|
138
|
+
sha256?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Closed per-field limits shared by ingestion and versioned output schemas. */
|
|
142
|
+
export const RECORD_METADATA_LIMITS = {
|
|
143
|
+
maxItems: 256,
|
|
144
|
+
maxAdapterIdChars: 128,
|
|
145
|
+
maxAdapterVersionChars: 64,
|
|
146
|
+
maxTitleChars: 2048,
|
|
147
|
+
maxPersonChars: 2048,
|
|
148
|
+
maxCategoryChars: 512,
|
|
149
|
+
maxIdentifierChars: 2048,
|
|
150
|
+
maxDateFieldKeyChars: 128,
|
|
151
|
+
maxDateFieldValueChars: 256,
|
|
152
|
+
maxAttachmentNameChars: 512,
|
|
153
|
+
maxAttachmentMimeChars: 256,
|
|
154
|
+
maxAnchorChars: 512,
|
|
155
|
+
maxLanguageHintChars: 64,
|
|
156
|
+
} as const;
|
|
157
|
+
|
|
158
|
+
/** Metadata shared by export adapters and later search/get projections. */
|
|
159
|
+
export interface RecordMetadata {
|
|
160
|
+
author?: string;
|
|
161
|
+
participants?: string[];
|
|
162
|
+
categories?: string[];
|
|
163
|
+
dateFields?: Record<string, string>;
|
|
164
|
+
messageId?: string;
|
|
165
|
+
inReplyTo?: string;
|
|
166
|
+
references?: string[];
|
|
167
|
+
threadId?: string;
|
|
168
|
+
eventId?: string;
|
|
169
|
+
sessionId?: string;
|
|
170
|
+
attachments?: RecordAttachmentInventoryItem[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** One raw logical record yielded by a container adapter. */
|
|
174
|
+
export interface RecordAdapterRecord {
|
|
175
|
+
stableId: string;
|
|
176
|
+
sourceLocator: string;
|
|
177
|
+
markdown: string;
|
|
178
|
+
/** Optional exact source-item hash. Derived deterministically when omitted. */
|
|
179
|
+
sourceHash?: string;
|
|
180
|
+
title?: string;
|
|
181
|
+
languageHint?: string;
|
|
182
|
+
metadata?: RecordMetadata;
|
|
183
|
+
anchors?: RecordAnchor[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export type RecordAdapterFailureCode =
|
|
187
|
+
| "MALFORMED_RECORD"
|
|
188
|
+
| "MISSING_ID"
|
|
189
|
+
| "DUPLICATE_ID"
|
|
190
|
+
| "RECORD_TOO_LARGE"
|
|
191
|
+
| "SOURCE_TOO_LARGE"
|
|
192
|
+
| "TIMEOUT"
|
|
193
|
+
| "RECORD_LIMIT"
|
|
194
|
+
| "FAILURE_LIMIT"
|
|
195
|
+
| "INVALID_LOCATOR"
|
|
196
|
+
| "INVALID_SOURCE_HASH"
|
|
197
|
+
| "ADAPTER_FAILURE"
|
|
198
|
+
| "INVALID_SNAPSHOT";
|
|
199
|
+
|
|
200
|
+
export interface RecordAdapterFailure {
|
|
201
|
+
code: RecordAdapterFailureCode;
|
|
202
|
+
message: string;
|
|
203
|
+
retryable: boolean;
|
|
204
|
+
stableId?: string;
|
|
205
|
+
sourceLocator?: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export type RecordAdapterEvent =
|
|
209
|
+
| { type: "record"; record: RecordAdapterRecord }
|
|
210
|
+
| { type: "failure"; failure: RecordAdapterFailure }
|
|
211
|
+
| { type: "snapshot"; state: "complete" | "partial" };
|
|
212
|
+
|
|
213
|
+
/** Separate lane from one-file converters so byte-oriented behavior is stable. */
|
|
214
|
+
export interface RecordAdapter {
|
|
215
|
+
readonly id: string;
|
|
216
|
+
readonly version: string;
|
|
217
|
+
/** Stable fingerprint of declarative adapter configuration, when present. */
|
|
218
|
+
readonly configurationFingerprint?: string;
|
|
219
|
+
canHandle(mime: string, ext: string): boolean;
|
|
220
|
+
records(input: RecordAdapterInput): AsyncIterable<RecordAdapterEvent>;
|
|
221
|
+
}
|
|
222
|
+
|
|
97
223
|
/**
|
|
98
224
|
* Pipeline output after canonicalization and hash computation.
|
|
99
225
|
* This is what consumers receive from the conversion pipeline.
|
|
@@ -121,3 +247,13 @@ export const DEFAULT_LIMITS = {
|
|
|
121
247
|
timeoutMs: 60_000, // 60 seconds
|
|
122
248
|
maxOutputChars: 50_000_000, // 50M chars (zip bomb protection)
|
|
123
249
|
} as const;
|
|
250
|
+
|
|
251
|
+
export const DEFAULT_RECORD_ADAPTER_LIMITS: RecordAdapterLimits = {
|
|
252
|
+
timeoutMs: 60_000,
|
|
253
|
+
maxSourceBytes: 100 * 1024 * 1024,
|
|
254
|
+
maxRecordChars: 2_000_000,
|
|
255
|
+
maxMetadataChars: 100_000,
|
|
256
|
+
maxTotalChars: 50_000_000,
|
|
257
|
+
maxRecords: 100_000,
|
|
258
|
+
maxFailures: 1_000,
|
|
259
|
+
};
|