@company-semantics/contracts 97.0.0 → 99.0.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.
@@ -1,12 +1,6 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import {
3
- INGESTION_FORMATS,
4
- byMime,
5
- acceptExtensions,
6
- humanLabels,
7
- INGEST_ACCEPT,
8
- SUPPORTED_LABEL,
9
- } from "../registry";
2
+ import { INGESTION_FORMATS, byMime, acceptExtensions } from "../registry";
3
+ import { FORMAT_CLASSES } from "../types";
10
4
 
11
5
  describe("ingestion registry", () => {
12
6
  it("has a unique MIME per entry", () => {
@@ -28,17 +22,23 @@ describe("ingestion registry", () => {
28
22
  expect(byMime("application/x-unknown")).toBeUndefined();
29
23
  });
30
24
 
31
- it("derives INGEST_ACCEPT from every extension in registry order", () => {
32
- expect(INGEST_ACCEPT).toBe(acceptExtensions().join(","));
33
- expect(INGEST_ACCEPT).toBe(
34
- ".txt,.md,.html,.htm,.csv,.rtf,.docx,.doc,.pptx,.ppt,.xlsx,.xls,.odt,.ods,.odp,.epub,.pdf,.png,.jpg,.jpeg,.webp,.gif,.heic,.svg,.drawio,.mp3,.m4a,.wav,.aac,.mp4,.m4v,.mov",
35
- );
25
+ it("every format carries a known class", () => {
26
+ for (const entry of INGESTION_FORMATS)
27
+ expect(FORMAT_CLASSES).toContain(entry.class);
36
28
  });
37
29
 
38
- it("derives SUPPORTED_LABEL from the unique group labels", () => {
39
- expect(SUPPORTED_LABEL).toBe(`Supported: ${humanLabels().join(", ")}`);
40
- expect(SUPPORTED_LABEL).toBe(
41
- "Supported: Text, Markdown, HTML, CSV, RTF, Word, PowerPoint, Excel, OpenDocument, EPUB, PDF, Image, Diagram, Audio, Video",
42
- );
30
+ it("classes documents the way discovery does", () => {
31
+ expect(byMime("text/csv")!.class).toBe("sheets");
32
+ expect(byMime("application/epub+zip")!.class).toBe("docs");
33
+ expect(byMime("application/vnd.ms-powerpoint")!.class).toBe("slides");
34
+ expect(byMime("application/pdf")!.class).toBe("pdf");
35
+ expect(byMime("text/markdown")!.class).toBe("text");
36
+ expect(byMime("image/svg+xml")!.class).toBe("diagram");
37
+ });
38
+
39
+ it("admits HEIF and no longer lists drawio", () => {
40
+ expect(byMime("image/heif")?.family).toBe("image");
41
+ expect(byMime("application/vnd.jgraph.mxfile")).toBeUndefined();
42
+ expect(acceptExtensions()).not.toContain(".drawio");
43
43
  });
44
44
  });
@@ -1,64 +1,92 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { INGESTION_FORMATS, byMime } from "../registry";
2
+ import { byMime } from "../registry";
3
+ import { TREATMENTS } from "../types";
4
+ import type { IngestionSurfaceKey } from "../types";
3
5
  import {
4
6
  INGESTION_SURFACES,
7
+ familiesOf,
8
+ treatmentFor,
5
9
  formatsFor,
6
- surfaceAdmits,
7
10
  acceptFor,
8
- supportedLabelFor,
9
11
  maxBytesFor,
10
12
  } from "../surfaces";
11
13
 
12
14
  describe("ingestion surfaces", () => {
13
- it("every registry entry carries a family", () => {
14
- for (const entry of INGESTION_FORMATS) {
15
- expect(entry.family).toMatch(
16
- /^(text|document|image|diagram|audio|video)$/,
17
- );
15
+ it("every surface declares treatments only for real families and a positive ceiling", () => {
16
+ for (const key of Object.keys(
17
+ INGESTION_SURFACES,
18
+ ) as IngestionSurfaceKey[]) {
19
+ const s = INGESTION_SURFACES[key];
20
+ expect(s.key).toBe(key);
21
+ expect(s.maxBytes).toBeGreaterThan(0);
22
+ for (const [family, treatment] of Object.entries(s.treatments)) {
23
+ expect([
24
+ "text",
25
+ "document",
26
+ "image",
27
+ "diagram",
28
+ "audio",
29
+ "video",
30
+ ]).toContain(family);
31
+ expect(TREATMENTS).toContain(treatment);
32
+ }
33
+ expect(familiesOf(key).length).toBeGreaterThan(0);
18
34
  }
19
35
  });
20
36
 
21
- it("every surface admits at least one format, in registry order", () => {
22
- for (const key of Object.keys(INGESTION_SURFACES) as Array<
23
- keyof typeof INGESTION_SURFACES
24
- >) {
25
- const formats = formatsFor(key);
26
- expect(formats.length).toBeGreaterThan(0);
27
- const order = formats.map((f) => INGESTION_FORMATS.indexOf(f));
28
- expect(order).toEqual([...order].sort((a, b) => a - b));
29
- }
37
+ it("company-md turns text and documents into text and refuses images and media", () => {
38
+ expect(treatmentFor("company-md", byMime("application/pdf")!)).toBe("text");
39
+ expect(treatmentFor("company-md", byMime("text/csv")!)).toBe("text");
40
+ expect(treatmentFor("company-md", byMime("image/png")!)).toBeNull();
41
+ expect(treatmentFor("company-md", byMime("audio/mpeg")!)).toBeNull();
42
+ expect(maxBytesFor("company-md")).toBe(50_000_000);
30
43
  });
31
44
 
32
- it("company-md admits text and documents only", () => {
33
- expect(surfaceAdmits("company-md", byMime("application/pdf")!)).toBe(true);
34
- expect(surfaceAdmits("company-md", byMime("text/csv")!)).toBe(true);
35
- expect(surfaceAdmits("company-md", byMime("image/png")!)).toBe(false);
36
- expect(surfaceAdmits("company-md", byMime("audio/mpeg")!)).toBe(false);
37
- expect(acceptFor("company-md")).toBe(
38
- ".txt,.md,.html,.htm,.csv,.rtf,.docx,.doc,.pptx,.ppt,.xlsx,.xls,.odt,.ods,.odp,.epub,.pdf",
39
- );
40
- expect(supportedLabelFor("company-md")).toBe(
41
- "Supported: Text, Markdown, HTML, CSV, RTF, Word, PowerPoint, Excel, OpenDocument, EPUB, PDF",
45
+ it("org-chart hands documents, images and diagrams to vision", () => {
46
+ expect(treatmentFor("org-chart", byMime("application/pdf")!)).toBe(
47
+ "vision",
42
48
  );
49
+ expect(treatmentFor("org-chart", byMime("image/heic")!)).toBe("vision");
50
+ expect(treatmentFor("org-chart", byMime("image/svg+xml")!)).toBe("vision");
51
+ expect(treatmentFor("org-chart", byMime("video/mp4")!)).toBeNull();
52
+ expect(maxBytesFor("org-chart")).toBe(10 * 1024 * 1024);
53
+ });
54
+
55
+ it("chat admits text, documents and images but never media", () => {
56
+ expect(treatmentFor("chat", byMime("text/markdown")!)).toBe("text");
57
+ expect(treatmentFor("chat", byMime("application/pdf")!)).toBe("vision");
58
+ expect(treatmentFor("chat", byMime("image/png")!)).toBe("vision");
59
+ expect(treatmentFor("chat", byMime("audio/mpeg")!)).toBeNull();
60
+ expect(maxBytesFor("chat")).toBe(5 * 1024 * 1024);
61
+ });
62
+
63
+ it("chat caps files per turn and only chat declares a per-turn count", () => {
64
+ expect(INGESTION_SURFACES.chat.maxFilesPerTurn).toBe(8);
65
+ for (const key of ["company-md", "org-chart", "profile-image"] as const) {
66
+ expect(INGESTION_SURFACES[key].maxFilesPerTurn).toBeUndefined();
67
+ }
43
68
  });
44
69
 
45
- it("org-chart adds images and diagrams but never media", () => {
46
- expect(surfaceAdmits("org-chart", byMime("image/heic")!)).toBe(true);
47
- expect(surfaceAdmits("org-chart", byMime("image/svg+xml")!)).toBe(true);
48
- expect(surfaceAdmits("org-chart", byMime("video/mp4")!)).toBe(false);
49
- expect(acceptFor("org-chart")).toBe(
50
- ".txt,.md,.html,.htm,.csv,.rtf,.docx,.doc,.pptx,.ppt,.xlsx,.xls,.odt,.ods,.odp,.epub,.pdf,.png,.jpg,.jpeg,.webp,.gif,.heic,.svg,.drawio",
70
+ it("profile-image stores images only", () => {
71
+ expect(familiesOf("profile-image")).toEqual(["image"]);
72
+ expect(treatmentFor("profile-image", byMime("image/heif")!)).toBe("store");
73
+ expect(acceptFor("profile-image")).toBe(
74
+ ".png,image/png,.jpg,.jpeg,image/jpeg,.webp,image/webp,.gif,image/gif,.heic,image/heic,.heif,image/heif",
51
75
  );
76
+ expect(maxBytesFor("profile-image")).toBe(5 * 1024 * 1024);
52
77
  });
53
78
 
54
- it("derives the byte ceiling from the admitted formats and the surface cap", () => {
55
- expect(maxBytesFor("company-md")).toBe(50_000_000);
56
- expect(maxBytesFor("org-chart")).toBe(10 * 1024 * 1024);
57
- for (const key of ["company-md", "org-chart"] as const) {
58
- const ceiling = INGESTION_SURFACES[key].maxBytes ?? Infinity;
59
- expect(maxBytesFor(key)).toBeLessThanOrEqual(ceiling);
79
+ it("acceptFor lists every admitted extension and MIME, in registry order", () => {
80
+ for (const key of Object.keys(
81
+ INGESTION_SURFACES,
82
+ ) as IngestionSurfaceKey[]) {
83
+ const tokens = acceptFor(key).split(",");
84
+ for (const f of formatsFor(key)) {
85
+ expect(tokens).toContain(f.mime);
86
+ for (const ext of f.extensions) expect(tokens).toContain(ext);
87
+ }
60
88
  expect(maxBytesFor(key)).toBeLessThanOrEqual(
61
- Math.max(...formatsFor(key).map((f) => f.maxBytes)),
89
+ INGESTION_SURFACES[key].maxBytes,
62
90
  );
63
91
  }
64
92
  });
@@ -13,22 +13,40 @@ export type {
13
13
  IngestionFormat,
14
14
  IngestionSurface,
15
15
  IngestionSurfaceKey,
16
+ FormatClass,
17
+ InventoryClass,
18
+ Treatment,
19
+ IngestionProvider,
20
+ IngestionProviderKey,
21
+ ProviderNativeFormat,
16
22
  } from "./types";
17
23
 
24
+ export { FORMAT_CLASSES, TREATMENTS } from "./types";
25
+
18
26
  export {
19
27
  INGESTION_FORMATS,
20
28
  byMime,
21
29
  acceptExtensions,
22
30
  humanLabels,
23
- INGEST_ACCEPT,
24
- SUPPORTED_LABEL,
25
31
  } from "./registry";
26
32
 
27
33
  export {
28
34
  INGESTION_SURFACES,
35
+ familiesOf,
36
+ treatmentFor,
29
37
  formatsFor,
30
38
  surfaceAdmits,
31
39
  acceptFor,
32
40
  supportedLabelFor,
33
41
  maxBytesFor,
34
42
  } from "./surfaces";
43
+
44
+ export type { ProviderFormat } from "./providers";
45
+
46
+ export {
47
+ INGESTION_PROVIDERS,
48
+ providerFormatsFor,
49
+ classOfMime,
50
+ inventoryClassesFor,
51
+ labelOfMime,
52
+ } from "./providers";
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Ingestion Providers
3
+ *
4
+ * Provider vocabulary: the native types an external source hands us (exported
5
+ * to a registry format before ingestion) and its structural container MIMEs.
6
+ * Discovery's net, the Drive picker filter, inventory classes and display
7
+ * labels all derive from this plus the surface declarations — no backend or
8
+ * app module keeps its own Workspace MIME table.
9
+ */
10
+ import { INGESTION_FORMATS, byMime } from "./registry";
11
+ import { surfaceAdmits } from "./surfaces";
12
+ import {
13
+ FORMAT_CLASSES,
14
+ type FormatClass,
15
+ type IngestionFormat,
16
+ type IngestionProvider,
17
+ type IngestionProviderKey,
18
+ type IngestionSurfaceKey,
19
+ type InventoryClass,
20
+ type ProviderNativeFormat,
21
+ } from "./types";
22
+
23
+ /** Every provider's native types and container MIMEs, keyed by provider. */
24
+ export const INGESTION_PROVIDERS: Readonly<
25
+ Record<IngestionProviderKey, IngestionProvider>
26
+ > = {
27
+ "google-drive": {
28
+ key: "google-drive",
29
+ nativeFormats: [
30
+ {
31
+ mime: "application/vnd.google-apps.document",
32
+ label: "Google Doc",
33
+ exportsTo:
34
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
35
+ },
36
+ {
37
+ mime: "application/vnd.google-apps.presentation",
38
+ label: "Google Slides",
39
+ exportsTo:
40
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
41
+ },
42
+ {
43
+ mime: "application/vnd.google-apps.spreadsheet",
44
+ label: "Google Sheet",
45
+ exportsTo:
46
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
47
+ },
48
+ ],
49
+ containerMimes: {
50
+ folder: "application/vnd.google-apps.folder",
51
+ shortcut: "application/vnd.google-apps.shortcut",
52
+ },
53
+ },
54
+ };
55
+
56
+ /** One MIME a provider can hand the pipeline, with the registry format it lands as. */
57
+ export interface ProviderFormat {
58
+ /** The MIME as the provider lists it (native or a registry MIME). */
59
+ mime: string;
60
+ /** True for a provider-native type that is exported before ingestion. */
61
+ native: boolean;
62
+ /** The registry format the bytes arrive as (the export target for natives). */
63
+ format: IngestionFormat;
64
+ /** Display label: the native label, else the registry label. */
65
+ label: string;
66
+ }
67
+
68
+ function exportTarget(native: ProviderNativeFormat): IngestionFormat {
69
+ const target = byMime(native.exportsTo);
70
+ if (!target) {
71
+ throw new Error(
72
+ `ingestion provider: ${native.mime} exports to ${native.exportsTo}, which is not in INGESTION_FORMATS`,
73
+ );
74
+ }
75
+ return target;
76
+ }
77
+
78
+ /** Every MIME the provider can hand a surface (natives first, then registry order). */
79
+ export function providerFormatsFor(
80
+ provider: IngestionProviderKey,
81
+ surface: IngestionSurfaceKey,
82
+ ): ProviderFormat[] {
83
+ const natives: ProviderFormat[] = INGESTION_PROVIDERS[
84
+ provider
85
+ ].nativeFormats.map((native) => ({
86
+ mime: native.mime,
87
+ native: true,
88
+ format: exportTarget(native),
89
+ label: native.label,
90
+ }));
91
+ const downloads: ProviderFormat[] = INGESTION_FORMATS.map((format) => ({
92
+ mime: format.mime,
93
+ native: false,
94
+ format,
95
+ label: format.label,
96
+ }));
97
+ return [...natives, ...downloads].filter((pf) =>
98
+ surfaceAdmits(surface, pf.format),
99
+ );
100
+ }
101
+
102
+ /** The class of a provider-listed MIME; null for containers and unknown types. */
103
+ export function classOfMime(
104
+ provider: IngestionProviderKey,
105
+ mime: string,
106
+ ): FormatClass | null {
107
+ const native = INGESTION_PROVIDERS[provider].nativeFormats.find(
108
+ (n) => n.mime === mime,
109
+ );
110
+ if (native) return exportTarget(native).class;
111
+ return byMime(mime)?.class ?? null;
112
+ }
113
+
114
+ /**
115
+ * The inventory classes discovery can record for a provider feeding a surface,
116
+ * in canonical class order, plus "shortcut". The DB CHECK on
117
+ * source_inventory_items.mime_class is pinned to this.
118
+ */
119
+ export function inventoryClassesFor(
120
+ provider: IngestionProviderKey,
121
+ surface: IngestionSurfaceKey,
122
+ ): InventoryClass[] {
123
+ const present = new Set(
124
+ providerFormatsFor(provider, surface).map((pf) => pf.format.class),
125
+ );
126
+ return [...FORMAT_CLASSES.filter((c) => present.has(c)), "shortcut"];
127
+ }
128
+
129
+ /** Display label for a MIME (provider-native first when a provider is given); null if unknown. */
130
+ export function labelOfMime(
131
+ mime: string,
132
+ provider?: IngestionProviderKey,
133
+ ): string | null {
134
+ if (provider) {
135
+ const native = INGESTION_PROVIDERS[provider].nativeFormats.find(
136
+ (n) => n.mime === mime,
137
+ );
138
+ if (native) return native.label;
139
+ }
140
+ return byMime(mime)?.label ?? null;
141
+ }
@@ -2,15 +2,18 @@
2
2
  * Ingestion Format Registry
3
3
  *
4
4
  * THE single source of truth for the public document-ingestion accept-set. The
5
- * backend normalizer joins these entries with its private dispatch table; the
6
- * app derives its file-input `accept` attribute and supported-formats copy from
7
- * the helpers below. Neither side re-spells the extension or label list.
5
+ * backend normalizer joins these entries with its private dispatch table; each
6
+ * upload surface derives its file-input `accept` attribute and supported-formats
7
+ * copy from the surface helpers. Neither side re-spells the extension, label or
8
+ * class list.
8
9
  *
9
10
  * Invariants:
10
11
  * - Entries are ordered text → documents → images/diagrams → audio/video; the
11
12
  * backend's MIME-order test and the derived UI strings depend on this order.
12
- * - `INGEST_ACCEPT` and `SUPPORTED_LABEL` are DERIVED, never hand-maintained —
13
- * adding a format here updates both automatically.
13
+ * - Every entry declares its `class` (one of `FORMAT_CLASSES`); no consumer
14
+ * keeps its own extension → class table.
15
+ * - `.drawio` is not listed: no normalizer can rasterize a raw `<mxfile>`, so
16
+ * admitting it would be a lie. It returns when a rasterizer exists.
14
17
  *
15
18
  * @see ADR-CONT-075 for design rationale
16
19
  */
@@ -22,6 +25,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
22
25
  {
23
26
  mime: "text/plain",
24
27
  family: "text",
28
+ class: "text",
25
29
  extensions: [".txt"],
26
30
  label: "Text",
27
31
  maxBytes: 10_000_000,
@@ -29,6 +33,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
29
33
  {
30
34
  mime: "text/markdown",
31
35
  family: "text",
36
+ class: "text",
32
37
  extensions: [".md"],
33
38
  label: "Markdown",
34
39
  maxBytes: 10_000_000,
@@ -36,6 +41,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
36
41
  {
37
42
  mime: "text/html",
38
43
  family: "text",
44
+ class: "text",
39
45
  extensions: [".html", ".htm"],
40
46
  label: "HTML",
41
47
  maxBytes: 10_000_000,
@@ -43,6 +49,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
43
49
  {
44
50
  mime: "text/csv",
45
51
  family: "text",
52
+ class: "sheets",
46
53
  extensions: [".csv"],
47
54
  label: "CSV",
48
55
  maxBytes: 25_000_000,
@@ -50,6 +57,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
50
57
  {
51
58
  mime: "application/rtf",
52
59
  family: "document",
60
+ class: "docs",
53
61
  extensions: [".rtf"],
54
62
  label: "RTF",
55
63
  maxBytes: 25_000_000,
@@ -57,6 +65,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
57
65
  {
58
66
  mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
59
67
  family: "document",
68
+ class: "docs",
60
69
  extensions: [".docx"],
61
70
  label: "Word",
62
71
  maxBytes: 25_000_000,
@@ -64,6 +73,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
64
73
  {
65
74
  mime: "application/msword",
66
75
  family: "document",
76
+ class: "docs",
67
77
  extensions: [".doc"],
68
78
  label: "Word",
69
79
  maxBytes: 25_000_000,
@@ -71,6 +81,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
71
81
  {
72
82
  mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
73
83
  family: "document",
84
+ class: "slides",
74
85
  extensions: [".pptx"],
75
86
  label: "PowerPoint",
76
87
  maxBytes: 50_000_000,
@@ -78,6 +89,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
78
89
  {
79
90
  mime: "application/vnd.ms-powerpoint",
80
91
  family: "document",
92
+ class: "slides",
81
93
  extensions: [".ppt"],
82
94
  label: "PowerPoint",
83
95
  maxBytes: 50_000_000,
@@ -85,6 +97,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
85
97
  {
86
98
  mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
87
99
  family: "document",
100
+ class: "sheets",
88
101
  extensions: [".xlsx"],
89
102
  label: "Excel",
90
103
  maxBytes: 50_000_000,
@@ -92,6 +105,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
92
105
  {
93
106
  mime: "application/vnd.ms-excel",
94
107
  family: "document",
108
+ class: "sheets",
95
109
  extensions: [".xls"],
96
110
  label: "Excel",
97
111
  maxBytes: 50_000_000,
@@ -99,6 +113,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
99
113
  {
100
114
  mime: "application/vnd.oasis.opendocument.text",
101
115
  family: "document",
116
+ class: "docs",
102
117
  extensions: [".odt"],
103
118
  label: "OpenDocument",
104
119
  maxBytes: 25_000_000,
@@ -106,6 +121,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
106
121
  {
107
122
  mime: "application/vnd.oasis.opendocument.spreadsheet",
108
123
  family: "document",
124
+ class: "sheets",
109
125
  extensions: [".ods"],
110
126
  label: "OpenDocument",
111
127
  maxBytes: 50_000_000,
@@ -113,6 +129,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
113
129
  {
114
130
  mime: "application/vnd.oasis.opendocument.presentation",
115
131
  family: "document",
132
+ class: "slides",
116
133
  extensions: [".odp"],
117
134
  label: "OpenDocument",
118
135
  maxBytes: 50_000_000,
@@ -120,6 +137,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
120
137
  {
121
138
  mime: "application/epub+zip",
122
139
  family: "document",
140
+ class: "docs",
123
141
  extensions: [".epub"],
124
142
  label: "EPUB",
125
143
  maxBytes: 25_000_000,
@@ -127,6 +145,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
127
145
  {
128
146
  mime: "application/pdf",
129
147
  family: "document",
148
+ class: "pdf",
130
149
  extensions: [".pdf"],
131
150
  label: "PDF",
132
151
  maxBytes: 50_000_000,
@@ -134,6 +153,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
134
153
  {
135
154
  mime: "image/png",
136
155
  family: "image",
156
+ class: "image",
137
157
  extensions: [".png"],
138
158
  label: "Image",
139
159
  maxBytes: 25_000_000,
@@ -141,6 +161,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
141
161
  {
142
162
  mime: "image/jpeg",
143
163
  family: "image",
164
+ class: "image",
144
165
  extensions: [".jpg", ".jpeg"],
145
166
  label: "Image",
146
167
  maxBytes: 25_000_000,
@@ -148,6 +169,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
148
169
  {
149
170
  mime: "image/webp",
150
171
  family: "image",
172
+ class: "image",
151
173
  extensions: [".webp"],
152
174
  label: "Image",
153
175
  maxBytes: 25_000_000,
@@ -155,6 +177,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
155
177
  {
156
178
  mime: "image/gif",
157
179
  family: "image",
180
+ class: "image",
158
181
  extensions: [".gif"],
159
182
  label: "Image",
160
183
  maxBytes: 25_000_000,
@@ -162,27 +185,31 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
162
185
  {
163
186
  mime: "image/heic",
164
187
  family: "image",
188
+ class: "image",
165
189
  extensions: [".heic"],
166
190
  label: "Image",
167
191
  maxBytes: 25_000_000,
168
192
  },
169
193
  {
170
- mime: "image/svg+xml",
171
- family: "diagram",
172
- extensions: [".svg"],
173
- label: "Diagram",
174
- maxBytes: 10_000_000,
194
+ mime: "image/heif",
195
+ family: "image",
196
+ class: "image",
197
+ extensions: [".heif"],
198
+ label: "Image",
199
+ maxBytes: 25_000_000,
175
200
  },
176
201
  {
177
- mime: "application/vnd.jgraph.mxfile",
202
+ mime: "image/svg+xml",
178
203
  family: "diagram",
179
- extensions: [".drawio"],
204
+ class: "diagram",
205
+ extensions: [".svg"],
180
206
  label: "Diagram",
181
207
  maxBytes: 10_000_000,
182
208
  },
183
209
  {
184
210
  mime: "audio/mpeg",
185
211
  family: "audio",
212
+ class: "audio",
186
213
  extensions: [".mp3"],
187
214
  label: "Audio",
188
215
  maxBytes: 500_000_000,
@@ -190,6 +217,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
190
217
  {
191
218
  mime: "audio/x-m4a",
192
219
  family: "audio",
220
+ class: "audio",
193
221
  extensions: [".m4a"],
194
222
  label: "Audio",
195
223
  maxBytes: 500_000_000,
@@ -197,6 +225,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
197
225
  {
198
226
  mime: "audio/wav",
199
227
  family: "audio",
228
+ class: "audio",
200
229
  extensions: [".wav"],
201
230
  label: "Audio",
202
231
  maxBytes: 500_000_000,
@@ -204,6 +233,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
204
233
  {
205
234
  mime: "audio/aac",
206
235
  family: "audio",
236
+ class: "audio",
207
237
  extensions: [".aac"],
208
238
  label: "Audio",
209
239
  maxBytes: 500_000_000,
@@ -211,6 +241,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
211
241
  {
212
242
  mime: "video/mp4",
213
243
  family: "video",
244
+ class: "video",
214
245
  extensions: [".mp4", ".m4v"],
215
246
  label: "Video",
216
247
  maxBytes: 500_000_000,
@@ -218,6 +249,7 @@ export const INGESTION_FORMATS: readonly IngestionFormat[] = [
218
249
  {
219
250
  mime: "video/quicktime",
220
251
  family: "video",
252
+ class: "video",
221
253
  extensions: [".mov"],
222
254
  label: "Video",
223
255
  maxBytes: 500_000_000,
@@ -238,16 +270,3 @@ export function acceptExtensions(): string[] {
238
270
  export function humanLabels(): string[] {
239
271
  return [...new Set(INGESTION_FORMATS.map((entry) => entry.label))];
240
272
  }
241
-
242
- /**
243
- * Comma-joined extension string for a file input's `accept` attribute.
244
- * Derived from {@link acceptExtensions} — never hand-maintained.
245
- */
246
- export const INGEST_ACCEPT: string = acceptExtensions().join(",");
247
-
248
- /**
249
- * Human-readable supported-formats summary, derived from {@link humanLabels}.
250
- * Names every accepted group (in registry order) so the copy can't undersell
251
- * what actually works.
252
- */
253
- export const SUPPORTED_LABEL: string = `Supported: ${humanLabels().join(", ")}`;