@company-semantics/contracts 98.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.
- package/package.json +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +51 -2
- package/src/chat/__tests__/runtime-profile.test.ts +23 -5
- package/src/generated/openapi-routes.ts +1 -0
- package/src/ingestion/README.md +51 -21
- package/src/ingestion/__tests__/providers.test.ts +59 -0
- package/src/ingestion/__tests__/registry.test.ts +18 -18
- package/src/ingestion/__tests__/surfaces.test.ts +68 -40
- package/src/ingestion/index.ts +20 -2
- package/src/ingestion/providers.ts +141 -0
- package/src/ingestion/registry.ts +44 -25
- package/src/ingestion/surfaces.ts +81 -28
- package/src/ingestion/types.ts +80 -15
package/package.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = '935ee995d53d' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '935ee995d53d56538f0566bb3c1372041d5cb1def41f3b28db02716ad4d8c591' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -421,6 +421,23 @@ export interface paths {
|
|
|
421
421
|
patch?: never;
|
|
422
422
|
trace?: never;
|
|
423
423
|
};
|
|
424
|
+
"/api/chats/{id}/branch": {
|
|
425
|
+
parameters: {
|
|
426
|
+
query?: never;
|
|
427
|
+
header?: never;
|
|
428
|
+
path?: never;
|
|
429
|
+
cookie?: never;
|
|
430
|
+
};
|
|
431
|
+
get?: never;
|
|
432
|
+
put?: never;
|
|
433
|
+
/** Make one version of a turn the active branch */
|
|
434
|
+
post: operations["selectChatBranch"];
|
|
435
|
+
delete?: never;
|
|
436
|
+
options?: never;
|
|
437
|
+
head?: never;
|
|
438
|
+
patch?: never;
|
|
439
|
+
trace?: never;
|
|
440
|
+
};
|
|
424
441
|
"/api/chats/{id}/pin": {
|
|
425
442
|
parameters: {
|
|
426
443
|
query?: never;
|
|
@@ -5719,6 +5736,10 @@ export interface components {
|
|
|
5719
5736
|
title: string;
|
|
5720
5737
|
isAutoGenerated: boolean;
|
|
5721
5738
|
};
|
|
5739
|
+
ChatBranchSelectRequest: {
|
|
5740
|
+
/** Format: uuid */
|
|
5741
|
+
messageId: string;
|
|
5742
|
+
};
|
|
5722
5743
|
ChatSseEvent: {
|
|
5723
5744
|
/** @constant */
|
|
5724
5745
|
v: 1;
|
|
@@ -5987,14 +6008,16 @@ export interface components {
|
|
|
5987
6008
|
ChatMessageFeedbackResponse: {
|
|
5988
6009
|
/** @enum {string} */
|
|
5989
6010
|
rating: "up" | "down";
|
|
5990
|
-
reason:
|
|
6011
|
+
reason: ("incorrect" | "didnt_follow_instructions" | "not_helpful" | "missing_context" | "tool_or_workflow_issue" | "other") | null;
|
|
5991
6012
|
/** Format: date-time */
|
|
5992
6013
|
updatedAt: string;
|
|
5993
6014
|
};
|
|
5994
6015
|
PutChatMessageFeedbackRequest: {
|
|
5995
6016
|
/** @enum {string} */
|
|
5996
6017
|
rating: "up" | "down";
|
|
5997
|
-
|
|
6018
|
+
/** @enum {string} */
|
|
6019
|
+
reason?: "incorrect" | "didnt_follow_instructions" | "not_helpful" | "missing_context" | "tool_or_workflow_issue" | "other";
|
|
6020
|
+
comment?: string;
|
|
5998
6021
|
};
|
|
5999
6022
|
WorkspaceAccessResponse: {
|
|
6000
6023
|
hasAccess: boolean;
|
|
@@ -12463,6 +12486,32 @@ export interface operations {
|
|
|
12463
12486
|
};
|
|
12464
12487
|
};
|
|
12465
12488
|
};
|
|
12489
|
+
selectChatBranch: {
|
|
12490
|
+
parameters: {
|
|
12491
|
+
query?: never;
|
|
12492
|
+
header?: never;
|
|
12493
|
+
path: {
|
|
12494
|
+
id: string;
|
|
12495
|
+
};
|
|
12496
|
+
cookie?: never;
|
|
12497
|
+
};
|
|
12498
|
+
requestBody: {
|
|
12499
|
+
content: {
|
|
12500
|
+
"application/json": components["schemas"]["ChatBranchSelectRequest"];
|
|
12501
|
+
};
|
|
12502
|
+
};
|
|
12503
|
+
responses: {
|
|
12504
|
+
/** @description The chat with its new active branch */
|
|
12505
|
+
200: {
|
|
12506
|
+
headers: {
|
|
12507
|
+
[name: string]: unknown;
|
|
12508
|
+
};
|
|
12509
|
+
content: {
|
|
12510
|
+
"application/json": components["schemas"]["GetChatResponse"];
|
|
12511
|
+
};
|
|
12512
|
+
};
|
|
12513
|
+
};
|
|
12514
|
+
};
|
|
12466
12515
|
pinChat: {
|
|
12467
12516
|
parameters: {
|
|
12468
12517
|
query?: never;
|
|
@@ -81,13 +81,31 @@ function backendAgenticModelDefault(): string {
|
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* The chat model the factory declares to the provider resolver. The factory's
|
|
86
|
+
* CHAT_DEFAULT_MODEL reads `modelFor('chat.default')`, so the id lives on the
|
|
87
|
+
* `chat.default` entry of the backend's model use-site registry.
|
|
88
|
+
*
|
|
89
|
+
* Until backend PRD-01163 lands on its origin/main, the resolved backend (a
|
|
90
|
+
* detached checkout such as the PR gate reads origin/main) has no registry and
|
|
91
|
+
* still declares the literal in factory.ts. Read whichever declaration the
|
|
92
|
+
* resolved backend actually has — parity is asserted either way. Drop the
|
|
93
|
+
* factory.ts branch once the registry is on backend main.
|
|
94
|
+
*/
|
|
85
95
|
function backendChatModelDefault(): string {
|
|
96
|
+
if (!backend!.exists("src/runtimes/models/use-sites.ts")) {
|
|
97
|
+
return extractDeclaration(
|
|
98
|
+
readBackendSource("src", "chat", "orchestrator", "factory.ts"),
|
|
99
|
+
/CHAT_DEFAULT_MODEL\s*=\s*['"]([^'"]+)['"]/,
|
|
100
|
+
"CHAT_DEFAULT_MODEL",
|
|
101
|
+
"src/chat/orchestrator/factory.ts",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
86
104
|
return extractDeclaration(
|
|
87
|
-
readBackendSource("src", "
|
|
88
|
-
/
|
|
89
|
-
"
|
|
90
|
-
"src/
|
|
105
|
+
readBackendSource("src", "runtimes", "models", "use-sites.ts"),
|
|
106
|
+
/['"]chat\.default['"]\s*:\s*\{\s*modelId:\s*['"]([^'"]+)['"]/,
|
|
107
|
+
"the 'chat.default' use-site modelId",
|
|
108
|
+
"src/runtimes/models/use-sites.ts",
|
|
91
109
|
);
|
|
92
110
|
}
|
|
93
111
|
|
|
@@ -28,6 +28,7 @@ export const openApiRoutes = {
|
|
|
28
28
|
'/api/chats/proactive/{eventId}': ['GET'],
|
|
29
29
|
'/api/chats/resolve-launch': ['POST'],
|
|
30
30
|
'/api/chats/{id}': ['DELETE', 'GET', 'PATCH'],
|
|
31
|
+
'/api/chats/{id}/branch': ['POST'],
|
|
31
32
|
'/api/chats/{id}/generate-title': ['POST'],
|
|
32
33
|
'/api/chats/{id}/messages': ['POST'],
|
|
33
34
|
'/api/chats/{id}/messages/{messageId}': ['DELETE'],
|
package/src/ingestion/README.md
CHANGED
|
@@ -1,49 +1,79 @@
|
|
|
1
1
|
# ingestion/
|
|
2
2
|
|
|
3
|
-
Shared vocabulary for the
|
|
3
|
+
Shared vocabulary for the one ingestion pipeline: formats, surfaces, providers.
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
7
|
-
The single source of truth for which
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
The single source of truth for which files the platform ingests and what each
|
|
8
|
+
surface does with them. The format registry names every accepted format — its
|
|
9
|
+
MIME type, filename extensions, family, class, human label group and byte cap.
|
|
10
|
+
The surface declarations say, per consuming surface (company-md, org-chart,
|
|
11
|
+
chat, profile-image), which treatment each admitted family gets and the byte
|
|
12
|
+
ceiling. The provider vocabulary names the native types an external source
|
|
13
|
+
(Google Drive) hands the pipeline and the registry format each exports to. The
|
|
14
|
+
backend normalizer, discovery and the app upload UI derive everything they need
|
|
15
|
+
from these three declarations.
|
|
12
16
|
|
|
13
17
|
## Invariants
|
|
14
18
|
|
|
15
19
|
- `INGESTION_FORMATS` is the canonical list; `mime` is unique across entries.
|
|
16
|
-
|
|
20
|
+
Entries are ordered text → documents → images/diagrams → audio/video; the
|
|
17
21
|
backend's MIME-order test and the derived UI strings depend on this order.
|
|
18
|
-
-
|
|
19
|
-
|
|
22
|
+
- Every format carries a `family` and a required `class` (`FormatClass`:
|
|
23
|
+
`docs | slides | sheets | pdf | text | image | diagram | audio | video`). No
|
|
24
|
+
consumer keeps its own extension → class table.
|
|
25
|
+
- A surface (`INGESTION_SURFACES`) declares `treatments` — family →
|
|
26
|
+
`text | vision | transcript | store` — and a required `maxBytes`.
|
|
27
|
+
`familiesOf`, `formatsFor`, `surfaceAdmits`, `treatmentFor`, `acceptFor`,
|
|
28
|
+
`supportedLabelFor` and `maxBytesFor` are **derived** from that declaration.
|
|
29
|
+
Treatments are per family, never per format.
|
|
30
|
+
- `vision` means the richest native form: images as images, a PDF as a binary
|
|
31
|
+
document, text-native formats as text.
|
|
32
|
+
- A provider (`INGESTION_PROVIDERS`) declares its native types, their export
|
|
33
|
+
targets and labels, and its container MIMEs. `providerFormatsFor`,
|
|
34
|
+
`classOfMime`, `inventoryClassesFor` and `labelOfMime` are **derived**.
|
|
35
|
+
- `acceptFor` emits extensions and MIMEs (".png,image/png,…"), so a
|
|
36
|
+
MIME-only matcher can use the same string as a file-input `accept`.
|
|
37
|
+
- Nothing outside this domain names a MIME, an extension or a provider-native
|
|
38
|
+
type; consumers ask these helpers (ADR slug `single-ingestion-pipeline`).
|
|
20
39
|
- No normalizer-routing fields here (`canonicalKind`, `normalizerKey` are
|
|
21
|
-
backend-only). This package stays pure vocabulary
|
|
22
|
-
|
|
23
|
-
each upload surface admits, and `formatsFor` / `acceptFor` /
|
|
24
|
-
`supportedLabelFor` / `maxBytesFor` are **derived** from that declaration
|
|
25
|
-
(ADR-CONTRACTS-159). A surface never re-spells a MIME.
|
|
40
|
+
backend-only). This package stays pure vocabulary
|
|
41
|
+
(ADR slug `ingestion-surface-treatments-and-providers`).
|
|
26
42
|
|
|
27
43
|
<!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
|
|
28
44
|
|
|
29
45
|
## Public API
|
|
30
46
|
|
|
47
|
+
- `FORMAT_CLASSES` — The coarse, provider-agnostic kind of a format, used wherever the system classes a file without reading it…
|
|
48
|
+
- `FormatClass` _(type)_
|
|
31
49
|
- `INGESTION_FORMATS` — The canonical public format registry.
|
|
32
|
-
- `
|
|
33
|
-
- `
|
|
50
|
+
- `INGESTION_PROVIDERS` — Every provider's native types and container MIMEs, keyed by provider.
|
|
51
|
+
- `INGESTION_SURFACES` — The canonical surface declarations. - `company-md`: context bank, goals extraction, Source Sync landing.
|
|
34
52
|
- `IngestionFamily` _(type)_ — The semantic family a format belongs to.
|
|
35
53
|
- `IngestionFormat` _(type)_ — One accepted upload format, keyed by its canonical MIME type.
|
|
36
|
-
- `
|
|
37
|
-
- `
|
|
38
|
-
- `
|
|
54
|
+
- `IngestionProvider` _(type)_ — One provider: its native types and the structural container MIMEs it lists.
|
|
55
|
+
- `IngestionProviderKey` _(type)_ — External sources that hand the pipeline provider-native files.
|
|
56
|
+
- `IngestionSurface` _(type)_ — One surface: the treatment it applies per admitted family, and its byte ceiling.
|
|
57
|
+
- `IngestionSurfaceKey` _(type)_ — The surfaces that consume the registry.
|
|
58
|
+
- `InventoryClass` _(type)_ — A Source Sync inventory class: a format class, or a provider shortcut row.
|
|
59
|
+
- `ProviderFormat` _(type)_ — One MIME a provider can hand the pipeline, with the registry format it lands as.
|
|
60
|
+
- `ProviderNativeFormat` _(type)_ — A provider-native type that is exported to a registry format before ingestion.
|
|
61
|
+
- `TREATMENTS` — What a surface does with an admitted family.
|
|
62
|
+
- `Treatment` _(type)_
|
|
39
63
|
- `acceptExtensions` — All accepted filename extensions, in registry order.
|
|
40
|
-
- `acceptFor` —
|
|
64
|
+
- `acceptFor` — The `<input accept>` value for a surface: per format, its extensions then its MIME…
|
|
41
65
|
- `byMime` — Look up the entry for a canonical MIME; undefined means unsupported.
|
|
66
|
+
- `classOfMime` — The class of a provider-listed MIME; null for containers and unknown types.
|
|
67
|
+
- `familiesOf` — The families a surface admits, in canonical family order.
|
|
42
68
|
- `formatsFor` — The registry formats a surface admits, in registry order.
|
|
43
69
|
- `humanLabels` — Unique group labels for human display, in registry order.
|
|
44
|
-
- `
|
|
70
|
+
- `inventoryClassesFor` — The inventory classes discovery can record for a provider feeding a surface, in canonical class order, plus…
|
|
71
|
+
- `labelOfMime` — Display label for a MIME (provider-native first when a provider is given); null if unknown.
|
|
72
|
+
- `maxBytesFor` — The transport byte ceiling: the largest registry cap among admitted formats, tightened to the (required)…
|
|
73
|
+
- `providerFormatsFor` — Every MIME the provider can hand a surface (natives first, then registry order).
|
|
45
74
|
- `supportedLabelFor` — "Supported: …" copy naming every label group the surface admits.
|
|
46
75
|
- `surfaceAdmits` — True when the surface admits this registry format.
|
|
76
|
+
- `treatmentFor` — What the surface does with this format; null = the surface refuses it.
|
|
47
77
|
|
|
48
78
|
<!-- END GENERATED: readme-public-api -->
|
|
49
79
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
INGESTION_PROVIDERS,
|
|
4
|
+
providerFormatsFor,
|
|
5
|
+
classOfMime,
|
|
6
|
+
inventoryClassesFor,
|
|
7
|
+
labelOfMime,
|
|
8
|
+
} from "../providers";
|
|
9
|
+
import { byMime } from "../registry";
|
|
10
|
+
|
|
11
|
+
describe("ingestion providers", () => {
|
|
12
|
+
it("every Drive native type exports to a registry format", () => {
|
|
13
|
+
for (const n of INGESTION_PROVIDERS["google-drive"].nativeFormats) {
|
|
14
|
+
expect(byMime(n.exportsTo)).toBeDefined();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("the company-md Drive net is the three Workspace types plus the company-md formats", () => {
|
|
19
|
+
const net = providerFormatsFor("google-drive", "company-md");
|
|
20
|
+
expect(net.slice(0, 3).map((pf) => pf.mime)).toEqual([
|
|
21
|
+
"application/vnd.google-apps.document",
|
|
22
|
+
"application/vnd.google-apps.presentation",
|
|
23
|
+
"application/vnd.google-apps.spreadsheet",
|
|
24
|
+
]);
|
|
25
|
+
expect(net.map((pf) => pf.mime)).toContain("application/pdf");
|
|
26
|
+
expect(net.map((pf) => pf.mime)).not.toContain("image/png");
|
|
27
|
+
expect(new Set(net.map((pf) => pf.mime)).size).toBe(net.length);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("pins the company-md inventory classes the DB CHECK carries today", () => {
|
|
31
|
+
expect(inventoryClassesFor("google-drive", "company-md")).toEqual([
|
|
32
|
+
"docs",
|
|
33
|
+
"slides",
|
|
34
|
+
"sheets",
|
|
35
|
+
"pdf",
|
|
36
|
+
"text",
|
|
37
|
+
"shortcut",
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("classes native, downloaded and container MIMEs", () => {
|
|
42
|
+
expect(
|
|
43
|
+
classOfMime("google-drive", "application/vnd.google-apps.spreadsheet"),
|
|
44
|
+
).toBe("sheets");
|
|
45
|
+
expect(classOfMime("google-drive", "text/csv")).toBe("sheets");
|
|
46
|
+
expect(classOfMime("google-drive", "image/png")).toBe("image");
|
|
47
|
+
expect(
|
|
48
|
+
classOfMime("google-drive", "application/vnd.google-apps.folder"),
|
|
49
|
+
).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("labels native and registry MIMEs", () => {
|
|
53
|
+
expect(
|
|
54
|
+
labelOfMime("application/vnd.google-apps.document", "google-drive"),
|
|
55
|
+
).toBe("Google Doc");
|
|
56
|
+
expect(labelOfMime("application/pdf")).toBe("PDF");
|
|
57
|
+
expect(labelOfMime("application/x-unknown")).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
|
|
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("
|
|
32
|
-
|
|
33
|
-
|
|
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("
|
|
39
|
-
expect(
|
|
40
|
-
expect(
|
|
41
|
-
|
|
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 {
|
|
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
|
|
14
|
-
for (const
|
|
15
|
-
|
|
16
|
-
|
|
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("
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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("
|
|
33
|
-
expect(
|
|
34
|
-
|
|
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("
|
|
46
|
-
expect(
|
|
47
|
-
expect(
|
|
48
|
-
expect(
|
|
49
|
-
|
|
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("
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
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
|
-
|
|
89
|
+
INGESTION_SURFACES[key].maxBytes,
|
|
62
90
|
);
|
|
63
91
|
}
|
|
64
92
|
});
|
package/src/ingestion/index.ts
CHANGED
|
@@ -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;
|
|
6
|
-
*
|
|
7
|
-
* the helpers
|
|
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
|
-
* -
|
|
13
|
-
*
|
|
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/
|
|
171
|
-
family: "
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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: "
|
|
202
|
+
mime: "image/svg+xml",
|
|
178
203
|
family: "diagram",
|
|
179
|
-
|
|
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(", ")}`;
|
|
@@ -1,58 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ingestion Surfaces
|
|
3
3
|
*
|
|
4
|
-
* Per-surface views of the single format registry. A surface
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Per-surface views of the single format registry. A surface declares a
|
|
5
|
+
* treatment per family it consumes plus a required byte ceiling; everything
|
|
6
|
+
* else — the families, the formats, the file-input `accept` string, the
|
|
7
|
+
* "Supported: …" copy, the transport byte ceiling — is DERIVED from
|
|
7
8
|
* `INGESTION_FORMATS` through that declaration. No surface re-spells a MIME.
|
|
8
9
|
*
|
|
9
10
|
* Invariants:
|
|
10
11
|
* - Every surface admits at least one registry format.
|
|
11
12
|
* - `maxBytesFor` never exceeds the registry cap of any admitted format, and
|
|
12
|
-
* never exceeds the surface ceiling
|
|
13
|
+
* never exceeds the surface ceiling.
|
|
13
14
|
*
|
|
14
15
|
* @see ADR-CONTRACTS-159 for design rationale
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import { INGESTION_FORMATS } from "./registry";
|
|
18
19
|
import type {
|
|
20
|
+
IngestionFamily,
|
|
19
21
|
IngestionFormat,
|
|
20
22
|
IngestionSurface,
|
|
21
23
|
IngestionSurfaceKey,
|
|
24
|
+
Treatment,
|
|
22
25
|
} from "./types";
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
const MIB = 1024 * 1024;
|
|
28
|
+
|
|
29
|
+
/** Canonical family order; `familiesOf` returns families in this order. */
|
|
30
|
+
const FAMILY_ORDER: readonly IngestionFamily[] = [
|
|
31
|
+
"text",
|
|
32
|
+
"document",
|
|
33
|
+
"image",
|
|
34
|
+
"diagram",
|
|
35
|
+
"audio",
|
|
36
|
+
"video",
|
|
37
|
+
];
|
|
26
38
|
|
|
27
39
|
/**
|
|
28
40
|
* The canonical surface declarations.
|
|
29
41
|
*
|
|
30
|
-
* - `company-md`: context
|
|
31
|
-
* documents
|
|
32
|
-
*
|
|
33
|
-
* - `org-chart`:
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* per
|
|
42
|
+
* - `company-md`: context bank, goals extraction, Source Sync landing. Text and
|
|
43
|
+
* documents become text. (Image/diagram vision on this surface is a later,
|
|
44
|
+
* separate change: it needs its own extraction prompt and discovery eval.)
|
|
45
|
+
* - `org-chart`: HRIS / org-chart import. A photographed, drawn or PDF chart
|
|
46
|
+
* goes to a vision-capable model natively; 10 MiB bounds the bytes per upload.
|
|
47
|
+
* - `chat`: chat composer attachments. Text becomes text; a PDF and images go
|
|
48
|
+
* to the model natively. At most 8 files per turn. 5 MiB per file = the composer's per-file cap today;
|
|
49
|
+
* the request body ceiling (base64, several files) is derived from it by the
|
|
50
|
+
* chat PRD, not declared here. Audio/video are refused until a transcript
|
|
51
|
+
* path is wired.
|
|
52
|
+
* - `profile-image`: avatar and org-logo uploads. Images are stored, never
|
|
53
|
+
* extracted; 5 MiB matches the current avatar/logo cap.
|
|
37
54
|
*/
|
|
38
55
|
export const INGESTION_SURFACES: Readonly<
|
|
39
56
|
Record<IngestionSurfaceKey, IngestionSurface>
|
|
40
57
|
> = {
|
|
41
58
|
"company-md": {
|
|
42
59
|
key: "company-md",
|
|
43
|
-
|
|
60
|
+
treatments: { text: "text", document: "text" },
|
|
61
|
+
maxBytes: 50_000_000,
|
|
44
62
|
},
|
|
45
63
|
"org-chart": {
|
|
46
64
|
key: "org-chart",
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
treatments: {
|
|
66
|
+
text: "text",
|
|
67
|
+
document: "vision",
|
|
68
|
+
image: "vision",
|
|
69
|
+
diagram: "vision",
|
|
70
|
+
},
|
|
71
|
+
maxBytes: 10 * MIB,
|
|
72
|
+
},
|
|
73
|
+
chat: {
|
|
74
|
+
key: "chat",
|
|
75
|
+
treatments: { text: "text", document: "vision", image: "vision" },
|
|
76
|
+
maxBytes: 5 * MIB,
|
|
77
|
+
// The composer sets no maxFiles today (unbounded; only the 10 MiB body cap
|
|
78
|
+
// limits a turn). 8 bounds per-turn vision/billing cost and the derived body
|
|
79
|
+
// ceiling (8 × 5 MiB × 4/3 base64 + envelope, computed by the chat PRD) while
|
|
80
|
+
// covering the multi-screenshot case.
|
|
81
|
+
maxFilesPerTurn: 8,
|
|
82
|
+
},
|
|
83
|
+
"profile-image": {
|
|
84
|
+
key: "profile-image",
|
|
85
|
+
treatments: { image: "store" },
|
|
86
|
+
maxBytes: 5 * MIB,
|
|
49
87
|
},
|
|
50
88
|
};
|
|
51
89
|
|
|
52
|
-
/** The
|
|
53
|
-
export function
|
|
54
|
-
const {
|
|
55
|
-
return
|
|
90
|
+
/** The families a surface admits, in canonical family order. */
|
|
91
|
+
export function familiesOf(surface: IngestionSurfaceKey): IngestionFamily[] {
|
|
92
|
+
const { treatments } = INGESTION_SURFACES[surface];
|
|
93
|
+
return FAMILY_ORDER.filter((family) => treatments[family] !== undefined);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** What the surface does with this format; null = the surface refuses it. */
|
|
97
|
+
export function treatmentFor(
|
|
98
|
+
surface: IngestionSurfaceKey,
|
|
99
|
+
format: IngestionFormat,
|
|
100
|
+
): Treatment | null {
|
|
101
|
+
return INGESTION_SURFACES[surface].treatments[format.family] ?? null;
|
|
56
102
|
}
|
|
57
103
|
|
|
58
104
|
/** True when the surface admits this registry format. */
|
|
@@ -60,13 +106,22 @@ export function surfaceAdmits(
|
|
|
60
106
|
surface: IngestionSurfaceKey,
|
|
61
107
|
format: IngestionFormat,
|
|
62
108
|
): boolean {
|
|
63
|
-
return
|
|
109
|
+
return treatmentFor(surface, format) !== null;
|
|
64
110
|
}
|
|
65
111
|
|
|
66
|
-
/**
|
|
112
|
+
/** The registry formats a surface admits, in registry order. */
|
|
113
|
+
export function formatsFor(surface: IngestionSurfaceKey): IngestionFormat[] {
|
|
114
|
+
return INGESTION_FORMATS.filter((entry) => surfaceAdmits(surface, entry));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The `<input accept>` value for a surface: per format, its extensions then its
|
|
119
|
+
* MIME (".png,image/png,.jpg,.jpeg,image/jpeg,…"). Both forms are valid in
|
|
120
|
+
* `accept`, and a MIME-only matcher (the chat composer) can use the same string.
|
|
121
|
+
*/
|
|
67
122
|
export function acceptFor(surface: IngestionSurfaceKey): string {
|
|
68
123
|
return formatsFor(surface)
|
|
69
|
-
.flatMap((entry) => [...entry.extensions])
|
|
124
|
+
.flatMap((entry) => [...entry.extensions, entry.mime])
|
|
70
125
|
.join(",");
|
|
71
126
|
}
|
|
72
127
|
|
|
@@ -77,15 +132,13 @@ export function supportedLabelFor(surface: IngestionSurfaceKey): string {
|
|
|
77
132
|
}
|
|
78
133
|
|
|
79
134
|
/**
|
|
80
|
-
* The transport byte ceiling
|
|
81
|
-
*
|
|
82
|
-
* The per-format registry cap still applies underneath it.
|
|
135
|
+
* The transport byte ceiling: the largest registry cap among admitted formats,
|
|
136
|
+
* tightened to the (required) surface ceiling.
|
|
83
137
|
*/
|
|
84
138
|
export function maxBytesFor(surface: IngestionSurfaceKey): number {
|
|
85
139
|
const registryMax = formatsFor(surface).reduce(
|
|
86
140
|
(max, entry) => Math.max(max, entry.maxBytes),
|
|
87
141
|
0,
|
|
88
142
|
);
|
|
89
|
-
|
|
90
|
-
return ceiling === undefined ? registryMax : Math.min(registryMax, ceiling);
|
|
143
|
+
return Math.min(registryMax, INGESTION_SURFACES[surface].maxBytes);
|
|
91
144
|
}
|
package/src/ingestion/types.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* private dispatch fields locally) and the app upload UI (which derives its
|
|
8
8
|
* `accept` attribute and supported-formats copy from this).
|
|
9
9
|
*
|
|
10
|
-
* Types
|
|
10
|
+
* Types plus the `FORMAT_CLASSES` and `TREATMENTS` vocabulary constants. The backend-only normalizer routing
|
|
11
11
|
* (`canonicalKind`, `normalizerKey`) deliberately does NOT live here; it is an
|
|
12
12
|
* implementation detail of the backend dispatch and would pollute the shared
|
|
13
13
|
* vocabulary.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* The semantic family a format belongs to. A SURFACE (an upload picker and the
|
|
20
|
-
* route behind it) declares
|
|
20
|
+
* route behind it) declares a treatment per family it consumes; the accept-set and the
|
|
21
21
|
* byte ceiling for that surface derive from the registry through this field.
|
|
22
22
|
* Public and semantic — NOT the backend normalizer dispatch (`canonicalKind` /
|
|
23
23
|
* `normalizerKey` stay backend-local, ADR-CONTRACTS-075).
|
|
@@ -27,6 +27,29 @@
|
|
|
27
27
|
export type IngestionFamily =
|
|
28
28
|
"text" | "document" | "image" | "diagram" | "audio" | "video";
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The coarse, provider-agnostic kind of a format, used wherever the system
|
|
32
|
+
* classes a file without reading it: the Source Sync inventory `mime_class`,
|
|
33
|
+
* the discovery topology mix, probe groups, and the spreadsheet `source_kind`.
|
|
34
|
+
* Declared ON the format entry — no consumer keeps an extension → class table.
|
|
35
|
+
* Canonical order (the inventory CHECK and every derived list follow it).
|
|
36
|
+
*/
|
|
37
|
+
export const FORMAT_CLASSES = [
|
|
38
|
+
"docs",
|
|
39
|
+
"slides",
|
|
40
|
+
"sheets",
|
|
41
|
+
"pdf",
|
|
42
|
+
"text",
|
|
43
|
+
"image",
|
|
44
|
+
"diagram",
|
|
45
|
+
"audio",
|
|
46
|
+
"video",
|
|
47
|
+
] as const;
|
|
48
|
+
export type FormatClass = (typeof FORMAT_CLASSES)[number];
|
|
49
|
+
|
|
50
|
+
/** A Source Sync inventory class: a format class, or a provider shortcut row. */
|
|
51
|
+
export type InventoryClass = FormatClass | "shortcut";
|
|
52
|
+
|
|
30
53
|
/**
|
|
31
54
|
* One accepted upload format, keyed by its canonical MIME type.
|
|
32
55
|
*
|
|
@@ -35,14 +58,17 @@ export type IngestionFamily =
|
|
|
35
58
|
* - `extensions` are lowercase, dot-prefixed (e.g. `.docx`).
|
|
36
59
|
* - `label` is the human display GROUP and is intentionally shared across
|
|
37
60
|
* related formats (`.docx` + `.doc` are both `Word`; the rasters are all
|
|
38
|
-
* `Image`; `.svg`
|
|
61
|
+
* `Image`; `.svg` is `Diagram`).
|
|
39
62
|
* - `family` is the semantic family a surface selects on; every entry has one.
|
|
63
|
+
* - `class` is the coarse class (one of `FORMAT_CLASSES`); every entry has one.
|
|
40
64
|
*/
|
|
41
65
|
export interface IngestionFormat {
|
|
42
66
|
/** Canonical MIME type (the registry key). */
|
|
43
67
|
mime: string;
|
|
44
|
-
/** Semantic family the format belongs to (what a surface
|
|
68
|
+
/** Semantic family the format belongs to (what a surface declares a treatment for). */
|
|
45
69
|
family: IngestionFamily;
|
|
70
|
+
/** Coarse class for classing without reading (inventory, topology, probes). */
|
|
71
|
+
class: FormatClass;
|
|
46
72
|
/** Lowercase, dot-prefixed filename extensions for this format. */
|
|
47
73
|
extensions: readonly string[];
|
|
48
74
|
/** Human display group label (e.g. `Word`, `Image`, `Audio`). */
|
|
@@ -51,23 +77,62 @@ export interface IngestionFormat {
|
|
|
51
77
|
maxBytes: number;
|
|
52
78
|
}
|
|
53
79
|
|
|
54
|
-
/** The upload surfaces that consume the registry. */
|
|
55
|
-
export type IngestionSurfaceKey = "company-md" | "org-chart";
|
|
56
|
-
|
|
57
80
|
/**
|
|
58
|
-
* What
|
|
59
|
-
*
|
|
81
|
+
* What a surface does with an admitted family. The backend proves at load that
|
|
82
|
+
* it can perform every declared (surface, family, treatment) for every admitted
|
|
83
|
+
* format, or it refuses to start.
|
|
60
84
|
*
|
|
61
|
-
*
|
|
85
|
+
* - `text`: normalize to text only (a PDF is text-extracted).
|
|
86
|
+
* - `vision`: hand the model the richest native form — images as image parts,
|
|
87
|
+
* a PDF as a binary document, text-native formats (docx, csv, …) as text.
|
|
88
|
+
* - `transcript`: audio/video become a transcript.
|
|
89
|
+
* - `store`: bytes are kept as-is (after format-preserving preparation such as
|
|
90
|
+
* HEIC → JPEG); nothing is extracted.
|
|
91
|
+
*/
|
|
92
|
+
export const TREATMENTS = ["text", "vision", "transcript", "store"] as const;
|
|
93
|
+
export type Treatment = (typeof TREATMENTS)[number];
|
|
94
|
+
|
|
95
|
+
/** The surfaces that consume the registry. */
|
|
96
|
+
export type IngestionSurfaceKey =
|
|
97
|
+
"company-md" | "org-chart" | "chat" | "profile-image";
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* One surface: the treatment it applies per admitted family, and its byte
|
|
101
|
+
* ceiling. `families` is derived (`familiesOf`), never declared.
|
|
62
102
|
*/
|
|
63
103
|
export interface IngestionSurface {
|
|
64
104
|
/** The surface this declaration describes. */
|
|
65
105
|
key: IngestionSurfaceKey;
|
|
66
|
-
/**
|
|
67
|
-
|
|
106
|
+
/** Admitted family → what the surface does with it. Absent family = refused (415). */
|
|
107
|
+
treatments: Readonly<Partial<Record<IngestionFamily, Treatment>>>;
|
|
108
|
+
/** Surface byte ceiling (required). Caps every admitted format below its registry `maxBytes`. */
|
|
109
|
+
maxBytes: number;
|
|
68
110
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
111
|
+
* Most files one turn may carry. Declared only on `chat` (required there,
|
|
112
|
+
* pinned by test); upload surfaces take one file per request and omit it.
|
|
71
113
|
*/
|
|
72
|
-
|
|
114
|
+
maxFilesPerTurn?: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** External sources that hand the pipeline provider-native files. */
|
|
118
|
+
export type IngestionProviderKey = "google-drive";
|
|
119
|
+
|
|
120
|
+
/** A provider-native type that is exported to a registry format before ingestion. */
|
|
121
|
+
export interface ProviderNativeFormat {
|
|
122
|
+
/** The provider's own MIME (e.g. a Google Workspace type). */
|
|
123
|
+
mime: string;
|
|
124
|
+
/** Human label for the native type (e.g. `Google Doc`). */
|
|
125
|
+
label: string;
|
|
126
|
+
/** Registry MIME the provider exports this type to. Must resolve via `byMime`. */
|
|
127
|
+
exportsTo: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** One provider: its native types and the structural container MIMEs it lists. */
|
|
131
|
+
export interface IngestionProvider {
|
|
132
|
+
/** The provider this declaration describes. */
|
|
133
|
+
key: IngestionProviderKey;
|
|
134
|
+
/** Native types the provider exports to registry formats. */
|
|
135
|
+
nativeFormats: readonly ProviderNativeFormat[];
|
|
136
|
+
/** Structural (non-file) MIMEs the provider lists: folders and shortcuts. */
|
|
137
|
+
containerMimes: { readonly folder: string; readonly shortcut: string };
|
|
73
138
|
}
|