@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.
Files changed (83) hide show
  1. package/README.md +2 -2
  2. package/assets/skill/SKILL.md +26 -1
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.27.0.zip → gno-browser-clipper-v1.28.0.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +2 -2
  7. package/spec/cli.md +41 -4
  8. package/spec/db/schema.sql +12 -0
  9. package/spec/mcp.md +5 -0
  10. package/spec/output-schemas/ask.schema.json +125 -0
  11. package/spec/output-schemas/context-capsule-v1.schema.json +94 -1
  12. package/spec/output-schemas/get.schema.json +94 -0
  13. package/spec/output-schemas/mcp-job-status.schema.json +28 -0
  14. package/spec/output-schemas/multi-get.schema.json +128 -0
  15. package/spec/output-schemas/record-import.schema.json +193 -0
  16. package/spec/output-schemas/search-result.schema.json +94 -0
  17. package/spec/output-schemas/search-results.schema.json +125 -0
  18. package/spec/project-profile.schema.json +66 -0
  19. package/src/app/context-format.ts +1 -1
  20. package/src/cli/commands/get.ts +12 -2
  21. package/src/cli/commands/index-cmd.ts +13 -0
  22. package/src/cli/commands/multi-get.ts +9 -2
  23. package/src/cli/commands/shared.ts +28 -0
  24. package/src/cli/commands/update.ts +5 -0
  25. package/src/cli/program.ts +4 -0
  26. package/src/config/project-profile.ts +2 -0
  27. package/src/config/types.ts +16 -0
  28. package/src/converters/adapters/browser-export/adapter.ts +199 -0
  29. package/src/converters/adapters/browser-export/formats.ts +358 -0
  30. package/src/converters/adapters/email/adapter.ts +429 -0
  31. package/src/converters/adapters/email/html.ts +162 -0
  32. package/src/converters/adapters/email/mime.ts +454 -0
  33. package/src/converters/adapters/email/parameters.ts +77 -0
  34. package/src/converters/adapters/ical/adapter.ts +475 -0
  35. package/src/converters/adapters/ical/recurrence.ts +186 -0
  36. package/src/converters/adapters/jsonl/adapter.ts +238 -0
  37. package/src/converters/adapters/jsonl/config.ts +105 -0
  38. package/src/converters/adapters/shared/html-text.ts +165 -0
  39. package/src/converters/adapters/shared/record-utils.ts +79 -0
  40. package/src/converters/adapters/shared/utf8-lines.ts +141 -0
  41. package/src/converters/adapters/transcript/adapter.ts +295 -0
  42. package/src/converters/adapters/transcript/json.ts +184 -0
  43. package/src/converters/adapters/transcript/model.ts +171 -0
  44. package/src/converters/adapters/transcript/text.ts +58 -0
  45. package/src/converters/adapters/transcript/timed.ts +152 -0
  46. package/src/converters/index.ts +11 -1
  47. package/src/converters/mime.ts +8 -0
  48. package/src/converters/pipeline.ts +15 -1
  49. package/src/converters/registry.ts +37 -1
  50. package/src/converters/types.ts +136 -0
  51. package/src/core/context-capsule-schema.ts +87 -0
  52. package/src/core/context-capsule.ts +20 -0
  53. package/src/core/context-evidence.ts +7 -1
  54. package/src/core/document-capabilities.ts +12 -0
  55. package/src/core/project-profile-apply-state.ts +5 -0
  56. package/src/core/project-profile.ts +4 -0
  57. package/src/core/record-metadata.ts +49 -0
  58. package/src/ingestion/record-adapter-canonical.ts +433 -0
  59. package/src/ingestion/record-adapter.ts +437 -0
  60. package/src/ingestion/record-container.ts +688 -0
  61. package/src/ingestion/record-path.ts +20 -0
  62. package/src/ingestion/record-sync.ts +70 -0
  63. package/src/ingestion/sync.ts +228 -36
  64. package/src/ingestion/types.ts +69 -1
  65. package/src/ingestion/walker.ts +31 -11
  66. package/src/mcp/tools/get.ts +10 -2
  67. package/src/mcp/tools/multi-get.ts +9 -2
  68. package/src/mcp/tools/workspace-write.ts +2 -0
  69. package/src/pipeline/filters.ts +1 -1
  70. package/src/pipeline/graph-retrieval.ts +7 -4
  71. package/src/pipeline/hybrid.ts +7 -4
  72. package/src/pipeline/result-context.ts +12 -4
  73. package/src/pipeline/search.ts +11 -4
  74. package/src/pipeline/types.ts +3 -0
  75. package/src/pipeline/vsearch.ts +26 -8
  76. package/src/sdk/documents.ts +13 -5
  77. package/src/serve/browse-tree.ts +4 -2
  78. package/src/serve/routes/api.ts +63 -65
  79. package/src/store/migrations/022-record-export-lineage.ts +42 -0
  80. package/src/store/migrations/index.ts +2 -0
  81. package/src/store/sqlite/adapter.ts +114 -7
  82. package/src/store/types.ts +40 -0
  83. package/browser-extension/artifacts/gno-browser-clipper-v1.27.0.zip.sha256 +0 -1
@@ -0,0 +1,141 @@
1
+ export type Utf8LineResult =
2
+ | {
3
+ ok: true;
4
+ lineNumber: number;
5
+ text: string;
6
+ terminated: boolean;
7
+ }
8
+ | {
9
+ ok: false;
10
+ lineNumber: number;
11
+ reason: "invalid_utf8" | "line_too_large";
12
+ terminated: boolean;
13
+ };
14
+
15
+ const BYTE_LF = 0x0a;
16
+ const BYTE_CR = 0x0d;
17
+ const UTF8_BOM = new Uint8Array([0xef, 0xbb, 0xbf]);
18
+
19
+ const concatenate = (
20
+ parts: readonly Uint8Array[],
21
+ byteLength: number
22
+ ): Uint8Array => {
23
+ if (parts.length === 1) return parts[0] ?? new Uint8Array();
24
+ const bytes = new Uint8Array(byteLength);
25
+ let offset = 0;
26
+ for (const part of parts) {
27
+ bytes.set(part, offset);
28
+ offset += part.byteLength;
29
+ }
30
+ return bytes;
31
+ };
32
+
33
+ const stripLineEnvelope = (
34
+ bytes: Uint8Array,
35
+ lineNumber: number
36
+ ): Uint8Array => {
37
+ let start = 0;
38
+ let end = bytes.byteLength;
39
+ if (
40
+ lineNumber === 1 &&
41
+ end >= UTF8_BOM.byteLength &&
42
+ UTF8_BOM.every((byte, index) => bytes[index] === byte)
43
+ ) {
44
+ start = UTF8_BOM.byteLength;
45
+ }
46
+ if (end > start && bytes[end - 1] === BYTE_CR) end -= 1;
47
+ return bytes.subarray(start, end);
48
+ };
49
+
50
+ const decodeLine = (
51
+ parts: readonly Uint8Array[],
52
+ byteLength: number,
53
+ lineNumber: number,
54
+ terminated: boolean
55
+ ): Utf8LineResult => {
56
+ const bytes = stripLineEnvelope(concatenate(parts, byteLength), lineNumber);
57
+ try {
58
+ return {
59
+ ok: true,
60
+ lineNumber,
61
+ text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
62
+ terminated,
63
+ };
64
+ } catch {
65
+ return {
66
+ ok: false,
67
+ lineNumber,
68
+ reason: "invalid_utf8",
69
+ terminated,
70
+ };
71
+ }
72
+ };
73
+
74
+ /**
75
+ * Split an async byte source without retaining an unbounded physical line.
76
+ * Invalid UTF-8 and oversized lines are isolated so later siblings can proceed.
77
+ */
78
+ export async function* readBoundedUtf8Lines(
79
+ source: AsyncIterable<Uint8Array>,
80
+ maxLineBytes: number
81
+ ): AsyncGenerator<Utf8LineResult> {
82
+ const safeMaxLineBytes = Math.max(1, Math.floor(maxLineBytes));
83
+ let parts: Uint8Array[] = [];
84
+ let lineBytes = 0;
85
+ let lineNumber = 1;
86
+ let oversized = false;
87
+
88
+ const reset = (): void => {
89
+ parts = [];
90
+ lineBytes = 0;
91
+ oversized = false;
92
+ lineNumber += 1;
93
+ };
94
+
95
+ for await (const chunk of source) {
96
+ let segmentStart = 0;
97
+ for (let index = 0; index < chunk.byteLength; index += 1) {
98
+ if (chunk[index] !== BYTE_LF) continue;
99
+ const segment = chunk.subarray(segmentStart, index);
100
+ if (!oversized && lineBytes + segment.byteLength <= safeMaxLineBytes) {
101
+ if (segment.byteLength > 0) parts.push(segment.slice());
102
+ lineBytes += segment.byteLength;
103
+ } else {
104
+ oversized = true;
105
+ }
106
+ if (oversized) {
107
+ yield {
108
+ ok: false,
109
+ lineNumber,
110
+ reason: "line_too_large",
111
+ terminated: true,
112
+ };
113
+ } else {
114
+ yield decodeLine(parts, lineBytes, lineNumber, true);
115
+ }
116
+ reset();
117
+ segmentStart = index + 1;
118
+ }
119
+
120
+ const remainder = chunk.subarray(segmentStart);
121
+ if (!oversized && lineBytes + remainder.byteLength <= safeMaxLineBytes) {
122
+ if (remainder.byteLength > 0) parts.push(remainder.slice());
123
+ lineBytes += remainder.byteLength;
124
+ } else if (remainder.byteLength > 0) {
125
+ oversized = true;
126
+ parts = [];
127
+ lineBytes = 0;
128
+ }
129
+ }
130
+
131
+ if (oversized) {
132
+ yield {
133
+ ok: false,
134
+ lineNumber,
135
+ reason: "line_too_large",
136
+ terminated: false,
137
+ };
138
+ } else if (lineBytes > 0) {
139
+ yield decodeLine(parts, lineBytes, lineNumber, false);
140
+ }
141
+ }
@@ -0,0 +1,295 @@
1
+ import type {
2
+ RecordAdapter,
3
+ RecordAdapterEvent,
4
+ RecordAdapterInput,
5
+ RecordAdapterRecord,
6
+ } from "../../types";
7
+ import type {
8
+ TranscriptAdapterOptions,
9
+ TranscriptFormat,
10
+ TranscriptParseEvent,
11
+ TranscriptSegment,
12
+ } from "./model";
13
+
14
+ import {
15
+ adapterLineByteLimit,
16
+ canonicalJson,
17
+ hashRecordValue,
18
+ safeInlineText,
19
+ sourceNamespace,
20
+ } from "../shared/record-utils";
21
+ import {
22
+ readBoundedUtf8Lines,
23
+ type Utf8LineResult,
24
+ } from "../shared/utf8-lines";
25
+ import { parseJsonTranscript } from "./json";
26
+ import { parseTranscriptAdapterOptions } from "./model";
27
+ import { parseTextTranscript } from "./text";
28
+ import { parseTimedTranscript } from "./timed";
29
+
30
+ const ADAPTER_ID = "adapter/transcript";
31
+ const ADAPTER_VERSION = "1.0.0";
32
+ const MAX_JSON_TRANSCRIPT_CHARS = 16 * 1024 * 1024;
33
+
34
+ const resolvedFormat = (
35
+ input: Pick<RecordAdapterInput, "ext" | "mime">,
36
+ requested: TranscriptFormat
37
+ ): Exclude<TranscriptFormat, "auto"> | undefined => {
38
+ if (requested !== "auto") return requested;
39
+ if (input.mime === "text/vtt" || input.ext === ".vtt") return "vtt";
40
+ if (
41
+ input.mime === "application/x-subrip" ||
42
+ input.mime === "text/srt" ||
43
+ input.ext === ".srt"
44
+ ) {
45
+ return "srt";
46
+ }
47
+ return undefined;
48
+ };
49
+
50
+ const failureEvent = (
51
+ event: Extract<TranscriptParseEvent, { ok: false }>
52
+ ): RecordAdapterEvent => ({
53
+ type: "failure",
54
+ failure: {
55
+ code: event.tooLarge ? "RECORD_TOO_LARGE" : "MALFORMED_RECORD",
56
+ message: "Transcript record could not be converted.",
57
+ retryable: event.retryable,
58
+ sourceLocator: event.sourceLocator,
59
+ },
60
+ });
61
+
62
+ const fileTitle = (relativePath: string): string => {
63
+ const name = relativePath.replaceAll("\\", "/").split("/").at(-1);
64
+ return (name?.replace(/\.[^.]+$/u, "") || "Transcript").normalize("NFC");
65
+ };
66
+
67
+ const localIdentity = (
68
+ segment: TranscriptSegment,
69
+ occurrences: Map<string, number>
70
+ ): string => {
71
+ if (segment.externalId) {
72
+ return `external:${hashRecordValue(
73
+ "gno-transcript-external-id-v1",
74
+ segment.externalId
75
+ )}`;
76
+ }
77
+ if (segment.start) {
78
+ const key = canonicalJson({
79
+ end: segment.end,
80
+ speaker: segment.speaker,
81
+ start: segment.start,
82
+ });
83
+ const count = (occurrences.get(key) ?? 0) + 1;
84
+ occurrences.set(key, count);
85
+ return `time:${hashRecordValue("gno-transcript-time-id-v1", key)}:${count}`;
86
+ }
87
+ if (segment.anchorKind === "record") {
88
+ return `record:${hashRecordValue(
89
+ "gno-transcript-record-id-v1",
90
+ segment.anchorValue
91
+ )}`;
92
+ }
93
+ return `content:${hashRecordValue(
94
+ "gno-transcript-content-id-v1",
95
+ canonicalJson({
96
+ speaker: segment.speaker,
97
+ text: segment.text,
98
+ })
99
+ )}`;
100
+ };
101
+
102
+ const segmentRecord = (
103
+ segment: TranscriptSegment,
104
+ input: RecordAdapterInput,
105
+ occurrences: Map<string, number>
106
+ ): RecordAdapterRecord => {
107
+ const sessionTitle = segment.sessionTitle ?? fileTitle(input.relativePath);
108
+ const label = segment.speaker
109
+ ? `${sessionTitle} — ${segment.speaker}`
110
+ : sessionTitle;
111
+ const details: string[] = [];
112
+ if (segment.speaker)
113
+ details.push(`**Speaker:** ${safeInlineText(segment.speaker)}`);
114
+ if (segment.start) {
115
+ details.push(
116
+ `**Time:** ${segment.start}${segment.end ? ` → ${segment.end}` : ""}`
117
+ );
118
+ }
119
+ const markdown = [
120
+ `# ${safeInlineText(label)}`,
121
+ ...details,
122
+ segment.text,
123
+ ].join("\n\n");
124
+ const participants = [
125
+ ...(segment.participants ?? []),
126
+ ...(segment.speaker ? [segment.speaker] : []),
127
+ ];
128
+ const uniqueParticipants = [
129
+ ...new Set(participants.map((value) => value.normalize("NFC").trim())),
130
+ ].filter(Boolean);
131
+ const anchors: RecordAdapterRecord["anchors"] = [
132
+ {
133
+ kind: segment.anchorKind,
134
+ value: segment.anchorValue,
135
+ endValue: segment.endAnchorValue,
136
+ },
137
+ ];
138
+ if (segment.start) {
139
+ anchors.push({
140
+ kind: "timestamp",
141
+ value: segment.start,
142
+ endValue: segment.end,
143
+ });
144
+ }
145
+ return {
146
+ stableId: `transcript:${sourceNamespace(input)}:${localIdentity(
147
+ segment,
148
+ occurrences
149
+ )}`,
150
+ sourceLocator: segment.sourceLocator,
151
+ sourceHash: hashRecordValue(
152
+ "gno-transcript-segment-source-v1",
153
+ canonicalJson(segment)
154
+ ),
155
+ title: label,
156
+ markdown,
157
+ metadata: {
158
+ author: segment.speaker,
159
+ participants:
160
+ uniqueParticipants.length > 0 ? uniqueParticipants : undefined,
161
+ categories: ["transcript"],
162
+ dateFields: segment.dateFields,
163
+ sessionId: segment.sessionId,
164
+ },
165
+ anchors,
166
+ };
167
+ };
168
+
169
+ const jsonEvents = async function* (
170
+ input: RecordAdapterInput
171
+ ): AsyncGenerator<TranscriptParseEvent> {
172
+ const parts: string[] = [];
173
+ let characters = 0;
174
+ let lastLine = 0;
175
+ const maxCharacters = Math.min(
176
+ MAX_JSON_TRANSCRIPT_CHARS,
177
+ input.limits.maxTotalChars
178
+ );
179
+ for await (const line of readBoundedUtf8Lines(
180
+ input.open(),
181
+ adapterLineByteLimit(input)
182
+ )) {
183
+ lastLine = line.lineNumber;
184
+ if (!line.ok) {
185
+ yield {
186
+ ok: false,
187
+ sourceLocator: `line:${line.lineNumber}`,
188
+ retryable: !line.terminated,
189
+ tooLarge: line.reason === "line_too_large",
190
+ };
191
+ return;
192
+ }
193
+ characters += line.text.length + 1;
194
+ if (characters > maxCharacters) {
195
+ yield {
196
+ ok: false,
197
+ sourceLocator: `lines:1-${line.lineNumber}`,
198
+ retryable: false,
199
+ tooLarge: true,
200
+ };
201
+ return;
202
+ }
203
+ parts.push(line.text);
204
+ }
205
+ if (parts.length === 0) {
206
+ yield {
207
+ ok: false,
208
+ sourceLocator: lastLine > 0 ? `lines:1-${lastLine}` : "record:root",
209
+ retryable: false,
210
+ };
211
+ return;
212
+ }
213
+ yield* parseJsonTranscript(parts.join("\n"));
214
+ };
215
+
216
+ const parseEvents = (
217
+ input: RecordAdapterInput,
218
+ format: Exclude<TranscriptFormat, "auto">
219
+ ): AsyncIterable<TranscriptParseEvent> => {
220
+ if (format === "json") return jsonEvents(input);
221
+ const lines: AsyncIterable<Utf8LineResult> = readBoundedUtf8Lines(
222
+ input.open(),
223
+ adapterLineByteLimit(input)
224
+ );
225
+ if (format === "text") return parseTextTranscript(lines);
226
+ return parseTimedTranscript(lines, format);
227
+ };
228
+
229
+ export const createTranscriptAdapter = (
230
+ options: TranscriptAdapterOptions = {}
231
+ ): RecordAdapter => {
232
+ const { format: requested } = parseTranscriptAdapterOptions(options);
233
+ return {
234
+ id: ADAPTER_ID,
235
+ version: ADAPTER_VERSION,
236
+ configurationFingerprint: hashRecordValue(
237
+ "gno-transcript-config-v1",
238
+ requested
239
+ ),
240
+ canHandle: (mime, ext) => {
241
+ if (requested === "json")
242
+ return mime === "application/json" || ext === ".json";
243
+ if (requested === "text") return mime === "text/plain" || ext === ".txt";
244
+ if (requested === "vtt") return mime === "text/vtt" || ext === ".vtt";
245
+ if (requested === "srt")
246
+ return (
247
+ mime === "application/x-subrip" ||
248
+ mime === "text/srt" ||
249
+ ext === ".srt"
250
+ );
251
+ return Boolean(resolvedFormat({ mime, ext }, requested));
252
+ },
253
+ records: async function* (
254
+ input: RecordAdapterInput
255
+ ): AsyncGenerator<RecordAdapterEvent> {
256
+ const format = resolvedFormat(input, requested);
257
+ if (!format) {
258
+ yield failureEvent({ ok: false, retryable: false });
259
+ yield { type: "snapshot", state: "partial" };
260
+ return;
261
+ }
262
+ const occurrences = new Map<string, number>();
263
+ let hadFailure = false;
264
+ try {
265
+ for await (const event of parseEvents(input, format)) {
266
+ if (!event.ok) {
267
+ hadFailure = true;
268
+ yield failureEvent(event);
269
+ continue;
270
+ }
271
+ yield {
272
+ type: "record",
273
+ record: segmentRecord(event.segment, input, occurrences),
274
+ };
275
+ }
276
+ } catch {
277
+ hadFailure = true;
278
+ yield {
279
+ type: "failure",
280
+ failure: {
281
+ code: "ADAPTER_FAILURE",
282
+ message: "Transcript source could not be read completely.",
283
+ retryable: true,
284
+ },
285
+ };
286
+ }
287
+ yield {
288
+ type: "snapshot",
289
+ state: hadFailure ? "partial" : "complete",
290
+ };
291
+ },
292
+ };
293
+ };
294
+
295
+ export const transcriptAdapter = createTranscriptAdapter();
@@ -0,0 +1,184 @@
1
+ import { scalarList, scalarText } from "../shared/record-utils";
2
+ import {
3
+ cleanTranscriptText,
4
+ parseTranscriptTimestamp,
5
+ type TranscriptParseEvent,
6
+ type TranscriptSegment,
7
+ } from "./model";
8
+
9
+ const ownValue = (
10
+ record: Record<string, unknown>,
11
+ names: readonly string[]
12
+ ): unknown => {
13
+ for (const name of names) {
14
+ if (Object.hasOwn(record, name)) return record[name];
15
+ }
16
+ return undefined;
17
+ };
18
+
19
+ const objectValue = (value: unknown): Record<string, unknown> | undefined =>
20
+ value && typeof value === "object" && !Array.isArray(value)
21
+ ? (value as Record<string, unknown>)
22
+ : undefined;
23
+
24
+ const personText = (value: unknown): string | undefined => {
25
+ const scalar = scalarText(value);
26
+ if (scalar) return scalar;
27
+ const object = objectValue(value);
28
+ return object
29
+ ? scalarText(ownValue(object, ["name", "displayName", "label", "id"]))
30
+ : undefined;
31
+ };
32
+
33
+ const peopleList = (value: unknown): string[] | undefined => {
34
+ if (!Array.isArray(value)) return scalarList(value);
35
+ const people = value
36
+ .map(personText)
37
+ .filter((person): person is string => Boolean(person));
38
+ return people.length > 0 ? people : undefined;
39
+ };
40
+
41
+ const segmentArray = (
42
+ root: unknown
43
+ ):
44
+ | {
45
+ segments: unknown[];
46
+ pointerName: string;
47
+ session: Record<string, unknown>;
48
+ }
49
+ | undefined => {
50
+ if (Array.isArray(root)) {
51
+ return { segments: root, pointerName: "", session: {} };
52
+ }
53
+ const rootObject = objectValue(root);
54
+ if (!rootObject) return undefined;
55
+ const nested = objectValue(ownValue(rootObject, ["transcript"]));
56
+ const session = nested ?? rootObject;
57
+ for (const name of ["segments", "utterances", "items"] as const) {
58
+ const value = ownValue(session, [name]);
59
+ if (Array.isArray(value)) {
60
+ return {
61
+ segments: value,
62
+ pointerName: nested ? `transcript/${name}` : name,
63
+ session,
64
+ };
65
+ }
66
+ }
67
+ return undefined;
68
+ };
69
+
70
+ const sessionDateFields = (
71
+ session: Record<string, unknown>
72
+ ): Record<string, string> | undefined => {
73
+ const fields: Record<string, string> = {};
74
+ for (const [name, aliases] of [
75
+ ["recorded", ["recordedAt", "recorded_at", "date"]],
76
+ ["created", ["createdAt", "created_at"]],
77
+ ] as const) {
78
+ const value = scalarText(ownValue(session, aliases));
79
+ if (value) fields[name] = value;
80
+ }
81
+ return Object.keys(fields).length > 0 ? fields : undefined;
82
+ };
83
+
84
+ export function* parseJsonTranscript(
85
+ text: string
86
+ ): Generator<TranscriptParseEvent> {
87
+ let parsed: unknown;
88
+ try {
89
+ parsed = JSON.parse(text);
90
+ } catch {
91
+ const finalCharacter = text.trimEnd().at(-1);
92
+ yield {
93
+ ok: false,
94
+ retryable: finalCharacter !== "}" && finalCharacter !== "]",
95
+ };
96
+ return;
97
+ }
98
+ const container = segmentArray(parsed);
99
+ if (!container) {
100
+ yield { ok: false, retryable: false };
101
+ return;
102
+ }
103
+
104
+ const sessionId = scalarText(
105
+ ownValue(container.session, ["sessionId", "session_id", "id"])
106
+ );
107
+ const sessionTitle = scalarText(
108
+ ownValue(container.session, ["title", "name"])
109
+ );
110
+ const participants = peopleList(
111
+ ownValue(container.session, ["participants", "speakers"])
112
+ );
113
+ const dateFields = sessionDateFields(container.session);
114
+
115
+ for (const [index, candidate] of container.segments.entries()) {
116
+ const pointer = `/${container.pointerName ? `${container.pointerName}/` : ""}${index}`;
117
+ const sourceLocator = `record:${pointer.slice(1) || index}`;
118
+ const record = objectValue(candidate);
119
+ if (!record) {
120
+ yield { ok: false, sourceLocator, retryable: false };
121
+ continue;
122
+ }
123
+ const rawText = scalarText(
124
+ ownValue(record, ["text", "content", "transcript"])
125
+ );
126
+ if (!rawText) {
127
+ yield { ok: false, sourceLocator, retryable: false };
128
+ continue;
129
+ }
130
+ const cleaned = cleanTranscriptText(rawText);
131
+ if (!cleaned.text) {
132
+ yield { ok: false, sourceLocator, retryable: false };
133
+ continue;
134
+ }
135
+
136
+ const rawStart = ownValue(record, [
137
+ "start",
138
+ "startTime",
139
+ "start_time",
140
+ "startSeconds",
141
+ ]);
142
+ const rawEnd = ownValue(record, [
143
+ "end",
144
+ "endTime",
145
+ "end_time",
146
+ "endSeconds",
147
+ ]);
148
+ const start =
149
+ rawStart === undefined ? undefined : parseTranscriptTimestamp(rawStart);
150
+ const end =
151
+ rawEnd === undefined ? undefined : parseTranscriptTimestamp(rawEnd);
152
+ if (
153
+ (rawStart !== undefined && !start) ||
154
+ (rawEnd !== undefined && !end) ||
155
+ (start && end && end.milliseconds < start.milliseconds)
156
+ ) {
157
+ yield { ok: false, sourceLocator, retryable: false };
158
+ continue;
159
+ }
160
+
161
+ const speaker =
162
+ personText(
163
+ ownValue(record, ["speaker", "speakerName", "speaker_name", "author"])
164
+ ) ?? cleaned.speaker;
165
+ const externalId = scalarText(
166
+ ownValue(record, ["id", "segmentId", "segment_id", "utteranceId"])
167
+ );
168
+ const segment: TranscriptSegment = {
169
+ externalId,
170
+ text: cleaned.text,
171
+ speaker,
172
+ start: start?.text,
173
+ end: end?.text,
174
+ sourceLocator,
175
+ anchorKind: "record",
176
+ anchorValue: pointer,
177
+ sessionId,
178
+ sessionTitle,
179
+ participants,
180
+ dateFields,
181
+ };
182
+ yield { ok: true, segment };
183
+ }
184
+ }