@company-semantics/contracts 13.16.0 → 13.18.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 +5 -1
- package/src/__tests__/resource-keys.test.ts +28 -0
- package/src/api/generated.ts +87 -0
- package/src/identity/people-org-chart.ts +1 -1
- package/src/index.ts +5 -0
- package/src/ingestion/README.md +35 -0
- package/src/ingestion/__tests__/registry.test.ts +44 -0
- package/src/ingestion/index.ts +19 -0
- package/src/ingestion/registry.ts +194 -0
- package/src/ingestion/types.ts +37 -0
- package/src/org/schemas.ts +1 -1
- package/src/resource-keys.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@company-semantics/contracts",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.18.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -64,6 +64,10 @@
|
|
|
64
64
|
"types": "./src/impersonation/index.ts",
|
|
65
65
|
"default": "./src/impersonation/index.ts"
|
|
66
66
|
},
|
|
67
|
+
"./ingestion": {
|
|
68
|
+
"types": "./src/ingestion/index.ts",
|
|
69
|
+
"default": "./src/ingestion/index.ts"
|
|
70
|
+
},
|
|
67
71
|
"./schemas/guard-result.schema.json": "./schemas/guard-result.schema.json"
|
|
68
72
|
},
|
|
69
73
|
"types": "./src/index.ts",
|
|
@@ -31,6 +31,34 @@ describe("resource-keys: orgUnitOwners (org-scoped)", () => {
|
|
|
31
31
|
});
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
describe("resource-keys: orgUnitOpenRoles (per-unit identity)", () => {
|
|
35
|
+
const UNIT_ID = "33333333-3333-4333-8333-333333333333";
|
|
36
|
+
const key: ResourceKey = {
|
|
37
|
+
type: "orgUnitOpenRoles",
|
|
38
|
+
orgId: ORG_ID,
|
|
39
|
+
unitId: UNIT_ID,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
it("toQueryKey produces [type, orgId, unitId]", () => {
|
|
43
|
+
const qk = toQueryKey(key);
|
|
44
|
+
expect(qk).toEqual(["orgUnitOpenRoles", ORG_ID, UNIT_ID]);
|
|
45
|
+
expect(qk).toHaveLength(3);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("roundtrips through fromQueryKey", () => {
|
|
49
|
+
const parsed = fromQueryKey(toQueryKey(key));
|
|
50
|
+
expect(parsed).toEqual(key);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("matchesResourceKey is unitId-scoped: rejects a different unit", () => {
|
|
54
|
+
const otherUnit = "44444444-4444-4444-8444-444444444444";
|
|
55
|
+
expect(matchesResourceKey(toQueryKey(key), key)).toBe(true);
|
|
56
|
+
expect(
|
|
57
|
+
matchesResourceKey(["orgUnitOpenRoles", ORG_ID, otherUnit], key),
|
|
58
|
+
).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
34
62
|
describe("resource-keys: system-scoped internalAdmin* types", () => {
|
|
35
63
|
const systemTypes = [
|
|
36
64
|
"internalAdminAiProviders",
|
package/src/api/generated.ts
CHANGED
|
@@ -1972,6 +1972,24 @@ export interface paths {
|
|
|
1972
1972
|
patch?: never;
|
|
1973
1973
|
trace?: never;
|
|
1974
1974
|
};
|
|
1975
|
+
"/api/org-units/{unitId}/head": {
|
|
1976
|
+
parameters: {
|
|
1977
|
+
query?: never;
|
|
1978
|
+
header?: never;
|
|
1979
|
+
path?: never;
|
|
1980
|
+
cookie?: never;
|
|
1981
|
+
};
|
|
1982
|
+
get?: never;
|
|
1983
|
+
/** Pin an explicit head (anchor person) for an org unit */
|
|
1984
|
+
put: operations["setOrgUnitHead"];
|
|
1985
|
+
post?: never;
|
|
1986
|
+
/** Revert an org unit head to the system-inferred default */
|
|
1987
|
+
delete: operations["revertOrgUnitHead"];
|
|
1988
|
+
options?: never;
|
|
1989
|
+
head?: never;
|
|
1990
|
+
patch?: never;
|
|
1991
|
+
trace?: never;
|
|
1992
|
+
};
|
|
1975
1993
|
"/api/org-units/{unitId}/my-authority": {
|
|
1976
1994
|
parameters: {
|
|
1977
1995
|
query?: never;
|
|
@@ -4287,6 +4305,8 @@ export interface components {
|
|
|
4287
4305
|
count: number;
|
|
4288
4306
|
userIds: string[];
|
|
4289
4307
|
} | null;
|
|
4308
|
+
headUserId: string | null;
|
|
4309
|
+
headOrigin: ("user" | "inferred") | null;
|
|
4290
4310
|
}[];
|
|
4291
4311
|
};
|
|
4292
4312
|
OrgUnitDescendantsResponse: {
|
|
@@ -4435,6 +4455,8 @@ export interface components {
|
|
|
4435
4455
|
count: number;
|
|
4436
4456
|
userIds: string[];
|
|
4437
4457
|
} | null;
|
|
4458
|
+
headUserId: string | null;
|
|
4459
|
+
headOrigin: ("user" | "inferred") | null;
|
|
4438
4460
|
}[];
|
|
4439
4461
|
levelConfig: {
|
|
4440
4462
|
/** Format: uuid */
|
|
@@ -4579,6 +4601,12 @@ export interface components {
|
|
|
4579
4601
|
revokedAt: string | null;
|
|
4580
4602
|
reason: string | null;
|
|
4581
4603
|
};
|
|
4604
|
+
UnitHeadResponse: {
|
|
4605
|
+
/** Format: uuid */
|
|
4606
|
+
unitId: string;
|
|
4607
|
+
headUserId: string | null;
|
|
4608
|
+
origin: ("user" | "inferred") | null;
|
|
4609
|
+
};
|
|
4582
4610
|
OrgUnitMyAuthorityResponse: {
|
|
4583
4611
|
/** Format: uuid */
|
|
4584
4612
|
unitId: string;
|
|
@@ -4693,6 +4721,14 @@ export interface components {
|
|
|
4693
4721
|
unitId: string;
|
|
4694
4722
|
title: string | null;
|
|
4695
4723
|
}[];
|
|
4724
|
+
unitHeads: {
|
|
4725
|
+
/** Format: uuid */
|
|
4726
|
+
unitId: string;
|
|
4727
|
+
/** Format: uuid */
|
|
4728
|
+
headUserId: string;
|
|
4729
|
+
/** @enum {string} */
|
|
4730
|
+
origin: "user" | "inferred";
|
|
4731
|
+
}[];
|
|
4696
4732
|
};
|
|
4697
4733
|
/** @description Polling snapshot of a generic ingestion operation. */
|
|
4698
4734
|
IngestionOperationPollResponse: {
|
|
@@ -8444,6 +8480,57 @@ export interface operations {
|
|
|
8444
8480
|
};
|
|
8445
8481
|
};
|
|
8446
8482
|
};
|
|
8483
|
+
setOrgUnitHead: {
|
|
8484
|
+
parameters: {
|
|
8485
|
+
query?: never;
|
|
8486
|
+
header?: never;
|
|
8487
|
+
path: {
|
|
8488
|
+
unitId: string;
|
|
8489
|
+
};
|
|
8490
|
+
cookie?: never;
|
|
8491
|
+
};
|
|
8492
|
+
requestBody: {
|
|
8493
|
+
content: {
|
|
8494
|
+
"application/json": {
|
|
8495
|
+
/** Format: uuid */
|
|
8496
|
+
userId: string;
|
|
8497
|
+
};
|
|
8498
|
+
};
|
|
8499
|
+
};
|
|
8500
|
+
responses: {
|
|
8501
|
+
/** @description The pinned unit head */
|
|
8502
|
+
200: {
|
|
8503
|
+
headers: {
|
|
8504
|
+
[name: string]: unknown;
|
|
8505
|
+
};
|
|
8506
|
+
content: {
|
|
8507
|
+
"application/json": components["schemas"]["UnitHeadResponse"];
|
|
8508
|
+
};
|
|
8509
|
+
};
|
|
8510
|
+
};
|
|
8511
|
+
};
|
|
8512
|
+
revertOrgUnitHead: {
|
|
8513
|
+
parameters: {
|
|
8514
|
+
query?: never;
|
|
8515
|
+
header?: never;
|
|
8516
|
+
path: {
|
|
8517
|
+
unitId: string;
|
|
8518
|
+
};
|
|
8519
|
+
cookie?: never;
|
|
8520
|
+
};
|
|
8521
|
+
requestBody?: never;
|
|
8522
|
+
responses: {
|
|
8523
|
+
/** @description The re-inferred unit head (or null when none resolves) */
|
|
8524
|
+
200: {
|
|
8525
|
+
headers: {
|
|
8526
|
+
[name: string]: unknown;
|
|
8527
|
+
};
|
|
8528
|
+
content: {
|
|
8529
|
+
"application/json": components["schemas"]["UnitHeadResponse"];
|
|
8530
|
+
};
|
|
8531
|
+
};
|
|
8532
|
+
};
|
|
8533
|
+
};
|
|
8447
8534
|
getMyOrgUnitAuthority: {
|
|
8448
8535
|
parameters: {
|
|
8449
8536
|
query?: never;
|
|
@@ -69,7 +69,7 @@ export type PeopleOrgChartOpenRole = z.infer<
|
|
|
69
69
|
// ---------------------------------------------------------------------------
|
|
70
70
|
// Unit head — the person a unit (and its person-less children: empty-unit
|
|
71
71
|
// placeholders, pending-invite ghosts, open-role seats) anchors under in the
|
|
72
|
-
// people chart. A presentation/anchor "tether" (ADR-BE-
|
|
72
|
+
// people chart. A presentation/anchor "tether" (ADR-BE-284), NOT authority.
|
|
73
73
|
// `origin` distinguishes an explicit user choice from a system-inferred default
|
|
74
74
|
// so the UI can render "Pinned" vs "Auto" and offer revert-to-auto. The client
|
|
75
75
|
// prefers the head of a unit's PARENT when anchoring its person-less children,
|
package/src/index.ts
CHANGED
|
@@ -779,3 +779,8 @@ export * from "./meetings";
|
|
|
779
779
|
// AccessLevel / OrgChartRole / AccessSource — see ADR-CTRL-084..088.
|
|
780
780
|
// No consumers in this PRD; cutover starts in AUTH-002 (PRD-00670).
|
|
781
781
|
export * from "./permissions";
|
|
782
|
+
|
|
783
|
+
// Document-ingestion accept-set vocabulary — see ADR-CONT-075.
|
|
784
|
+
// Single source of truth for upload formats; the backend normalizer joins its
|
|
785
|
+
// private dispatch fields locally, the app derives its accept-set + copy.
|
|
786
|
+
export * from "./ingestion";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# ingestion/
|
|
2
|
+
|
|
3
|
+
Shared vocabulary for the document-ingestion accept-set.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
The single source of truth for which file formats the platform accepts for
|
|
8
|
+
upload — their MIME types, filename extensions, human label groups, and
|
|
9
|
+
per-format byte caps. The backend normalizer joins these entries with its
|
|
10
|
+
private dispatch table; the app upload UI derives its file-input `accept`
|
|
11
|
+
attribute and supported-formats copy from the same data.
|
|
12
|
+
|
|
13
|
+
## Invariants
|
|
14
|
+
|
|
15
|
+
- `INGESTION_FORMATS` is the canonical list; `mime` is unique across entries.
|
|
16
|
+
- Entries are ordered text → documents → images/diagrams → audio/video. The
|
|
17
|
+
backend's MIME-order test and the derived UI strings depend on this order.
|
|
18
|
+
- `INGEST_ACCEPT` and `SUPPORTED_LABEL` are **derived** from the registry, never
|
|
19
|
+
hand-maintained — adding a format updates both automatically.
|
|
20
|
+
- No normalizer-routing fields here (`canonicalKind`, `normalizerKey` are
|
|
21
|
+
backend-only). This package stays pure vocabulary.
|
|
22
|
+
|
|
23
|
+
## Public API
|
|
24
|
+
|
|
25
|
+
- `IngestionFormat` — `{ mime, extensions, label, maxBytes }`.
|
|
26
|
+
- `INGESTION_FORMATS: readonly IngestionFormat[]`.
|
|
27
|
+
- `byMime(mime)`, `acceptExtensions()`, `humanLabels()`.
|
|
28
|
+
- `INGEST_ACCEPT: string` — comma-joined extensions for an `accept` attribute.
|
|
29
|
+
- `SUPPORTED_LABEL: string` — human-readable supported-formats summary.
|
|
30
|
+
|
|
31
|
+
## Dependencies
|
|
32
|
+
|
|
33
|
+
None beyond `./types`. (The backend registry at
|
|
34
|
+
`company-semantics-backend/src/orgs/ingestion/normalize/format-registry.ts`
|
|
35
|
+
consumes this and adds its private dispatch fields.)
|
|
@@ -0,0 +1,44 @@
|
|
|
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";
|
|
10
|
+
|
|
11
|
+
describe("ingestion registry", () => {
|
|
12
|
+
it("has a unique MIME per entry", () => {
|
|
13
|
+
const mimes = INGESTION_FORMATS.map((e) => e.mime);
|
|
14
|
+
expect(new Set(mimes).size).toBe(mimes.length);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("uses lowercase, dot-prefixed extensions", () => {
|
|
18
|
+
for (const entry of INGESTION_FORMATS) {
|
|
19
|
+
for (const ext of entry.extensions) {
|
|
20
|
+
expect(ext).toMatch(/^\.[a-z0-9]+$/);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("byMime resolves a known format and rejects unknown", () => {
|
|
26
|
+
expect(byMime("application/pdf")?.label).toBe("PDF");
|
|
27
|
+
expect(byMime("audio/mpeg")?.label).toBe("Audio");
|
|
28
|
+
expect(byMime("application/x-unknown")).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
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,.xlsx,.odt,.pdf,.png,.jpg,.jpeg,.webp,.gif,.heic,.svg,.drawio,.mp3,.m4a,.wav,.aac,.mp4,.m4v,.mov",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
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, PDF, Image, Diagram, Audio, Video",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingestion Domain Barrel
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the public document-ingestion format vocabulary.
|
|
5
|
+
* Import from '@company-semantics/contracts/ingestion'.
|
|
6
|
+
*
|
|
7
|
+
* @see ADR-CONT-075 for design rationale
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type { IngestionFormat } from "./types";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
INGESTION_FORMATS,
|
|
14
|
+
byMime,
|
|
15
|
+
acceptExtensions,
|
|
16
|
+
humanLabels,
|
|
17
|
+
INGEST_ACCEPT,
|
|
18
|
+
SUPPORTED_LABEL,
|
|
19
|
+
} from "./registry";
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingestion Format Registry
|
|
3
|
+
*
|
|
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.
|
|
8
|
+
*
|
|
9
|
+
* Invariants:
|
|
10
|
+
* - Entries are ordered text → documents → images/diagrams → audio/video; the
|
|
11
|
+
* 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.
|
|
14
|
+
*
|
|
15
|
+
* @see ADR-CONT-075 for design rationale
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { IngestionFormat } from "./types";
|
|
19
|
+
|
|
20
|
+
/** The canonical public format registry. */
|
|
21
|
+
export const INGESTION_FORMATS: readonly IngestionFormat[] = [
|
|
22
|
+
{
|
|
23
|
+
mime: "text/plain",
|
|
24
|
+
extensions: [".txt"],
|
|
25
|
+
label: "Text",
|
|
26
|
+
maxBytes: 10_000_000,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
mime: "text/markdown",
|
|
30
|
+
extensions: [".md"],
|
|
31
|
+
label: "Markdown",
|
|
32
|
+
maxBytes: 10_000_000,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
mime: "text/html",
|
|
36
|
+
extensions: [".html", ".htm"],
|
|
37
|
+
label: "HTML",
|
|
38
|
+
maxBytes: 10_000_000,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
mime: "text/csv",
|
|
42
|
+
extensions: [".csv"],
|
|
43
|
+
label: "CSV",
|
|
44
|
+
maxBytes: 25_000_000,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
mime: "application/rtf",
|
|
48
|
+
extensions: [".rtf"],
|
|
49
|
+
label: "RTF",
|
|
50
|
+
maxBytes: 25_000_000,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
54
|
+
extensions: [".docx"],
|
|
55
|
+
label: "Word",
|
|
56
|
+
maxBytes: 25_000_000,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
mime: "application/msword",
|
|
60
|
+
extensions: [".doc"],
|
|
61
|
+
label: "Word",
|
|
62
|
+
maxBytes: 25_000_000,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
66
|
+
extensions: [".pptx"],
|
|
67
|
+
label: "PowerPoint",
|
|
68
|
+
maxBytes: 50_000_000,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
72
|
+
extensions: [".xlsx"],
|
|
73
|
+
label: "Excel",
|
|
74
|
+
maxBytes: 50_000_000,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
mime: "application/vnd.oasis.opendocument.text",
|
|
78
|
+
extensions: [".odt"],
|
|
79
|
+
label: "OpenDocument",
|
|
80
|
+
maxBytes: 25_000_000,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
mime: "application/pdf",
|
|
84
|
+
extensions: [".pdf"],
|
|
85
|
+
label: "PDF",
|
|
86
|
+
maxBytes: 50_000_000,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
mime: "image/png",
|
|
90
|
+
extensions: [".png"],
|
|
91
|
+
label: "Image",
|
|
92
|
+
maxBytes: 25_000_000,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
mime: "image/jpeg",
|
|
96
|
+
extensions: [".jpg", ".jpeg"],
|
|
97
|
+
label: "Image",
|
|
98
|
+
maxBytes: 25_000_000,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
mime: "image/webp",
|
|
102
|
+
extensions: [".webp"],
|
|
103
|
+
label: "Image",
|
|
104
|
+
maxBytes: 25_000_000,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
mime: "image/gif",
|
|
108
|
+
extensions: [".gif"],
|
|
109
|
+
label: "Image",
|
|
110
|
+
maxBytes: 25_000_000,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
mime: "image/heic",
|
|
114
|
+
extensions: [".heic"],
|
|
115
|
+
label: "Image",
|
|
116
|
+
maxBytes: 25_000_000,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
mime: "image/svg+xml",
|
|
120
|
+
extensions: [".svg"],
|
|
121
|
+
label: "Diagram",
|
|
122
|
+
maxBytes: 10_000_000,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
mime: "application/vnd.jgraph.mxfile",
|
|
126
|
+
extensions: [".drawio"],
|
|
127
|
+
label: "Diagram",
|
|
128
|
+
maxBytes: 10_000_000,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
mime: "audio/mpeg",
|
|
132
|
+
extensions: [".mp3"],
|
|
133
|
+
label: "Audio",
|
|
134
|
+
maxBytes: 500_000_000,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
mime: "audio/x-m4a",
|
|
138
|
+
extensions: [".m4a"],
|
|
139
|
+
label: "Audio",
|
|
140
|
+
maxBytes: 500_000_000,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
mime: "audio/wav",
|
|
144
|
+
extensions: [".wav"],
|
|
145
|
+
label: "Audio",
|
|
146
|
+
maxBytes: 500_000_000,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
mime: "audio/aac",
|
|
150
|
+
extensions: [".aac"],
|
|
151
|
+
label: "Audio",
|
|
152
|
+
maxBytes: 500_000_000,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
mime: "video/mp4",
|
|
156
|
+
extensions: [".mp4", ".m4v"],
|
|
157
|
+
label: "Video",
|
|
158
|
+
maxBytes: 500_000_000,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
mime: "video/quicktime",
|
|
162
|
+
extensions: [".mov"],
|
|
163
|
+
label: "Video",
|
|
164
|
+
maxBytes: 500_000_000,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
/** Look up the entry for a canonical MIME; undefined means unsupported. */
|
|
169
|
+
export function byMime(mime: string): IngestionFormat | undefined {
|
|
170
|
+
return INGESTION_FORMATS.find((entry) => entry.mime === mime);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** All accepted filename extensions, in registry order. */
|
|
174
|
+
export function acceptExtensions(): string[] {
|
|
175
|
+
return INGESTION_FORMATS.flatMap((entry) => [...entry.extensions]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Unique group labels for human display, in registry order. */
|
|
179
|
+
export function humanLabels(): string[] {
|
|
180
|
+
return [...new Set(INGESTION_FORMATS.map((entry) => entry.label))];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Comma-joined extension string for a file input's `accept` attribute.
|
|
185
|
+
* Derived from {@link acceptExtensions} — never hand-maintained.
|
|
186
|
+
*/
|
|
187
|
+
export const INGEST_ACCEPT: string = acceptExtensions().join(",");
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Human-readable supported-formats summary, derived from {@link humanLabels}.
|
|
191
|
+
* Names every accepted group (in registry order) so the copy can't undersell
|
|
192
|
+
* what actually works.
|
|
193
|
+
*/
|
|
194
|
+
export const SUPPORTED_LABEL: string = `Supported: ${humanLabels().join(", ")}`;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingestion Format Types
|
|
3
|
+
*
|
|
4
|
+
* The public vocabulary for the document-ingestion accept-set: which file
|
|
5
|
+
* formats the platform accepts for upload, their human label groups, and the
|
|
6
|
+
* per-format byte caps. Shared by the backend normalizer (which adds its own
|
|
7
|
+
* private dispatch fields locally) and the app upload UI (which derives its
|
|
8
|
+
* `accept` attribute and supported-formats copy from this).
|
|
9
|
+
*
|
|
10
|
+
* Types only — no runtime behavior. The backend-only normalizer routing
|
|
11
|
+
* (`canonicalKind`, `normalizerKey`) deliberately does NOT live here; it is an
|
|
12
|
+
* implementation detail of the backend dispatch and would pollute the shared
|
|
13
|
+
* vocabulary.
|
|
14
|
+
*
|
|
15
|
+
* @see ADR-CONT-075 for design rationale
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One accepted upload format, keyed by its canonical MIME type.
|
|
20
|
+
*
|
|
21
|
+
* Invariants:
|
|
22
|
+
* - `mime` is unique across the registry.
|
|
23
|
+
* - `extensions` are lowercase, dot-prefixed (e.g. `.docx`).
|
|
24
|
+
* - `label` is the human display GROUP and is intentionally shared across
|
|
25
|
+
* related formats (`.docx` + `.doc` are both `Word`; the rasters are all
|
|
26
|
+
* `Image`; `.svg` + `.drawio` are `Diagram`).
|
|
27
|
+
*/
|
|
28
|
+
export interface IngestionFormat {
|
|
29
|
+
/** Canonical MIME type (the registry key). */
|
|
30
|
+
mime: string;
|
|
31
|
+
/** Lowercase, dot-prefixed filename extensions for this format. */
|
|
32
|
+
extensions: readonly string[];
|
|
33
|
+
/** Human display group label (e.g. `Word`, `Image`, `Audio`). */
|
|
34
|
+
label: string;
|
|
35
|
+
/** Per-format upload byte cap. */
|
|
36
|
+
maxBytes: number;
|
|
37
|
+
}
|
package/src/org/schemas.ts
CHANGED
|
@@ -948,7 +948,7 @@ export const OrgUnitTreeNodeSchema = OrgUnitSchema.extend({
|
|
|
948
948
|
openRoleCount: z.number().int().min(0),
|
|
949
949
|
missingAtNextLevel: MissingAtNextLevelSchema.nullable(),
|
|
950
950
|
/**
|
|
951
|
-
* The unit head (ADR-BE-
|
|
951
|
+
* The unit head (ADR-BE-284): the person this unit (and its person-less
|
|
952
952
|
* children) anchors under in the people chart — a presentation "tether", NOT
|
|
953
953
|
* authority. `headUserId` is null when no head is resolved (e.g. an empty
|
|
954
954
|
* unit with no members). `headOrigin` is `'user'` for an explicit choice,
|
package/src/resource-keys.ts
CHANGED
|
@@ -36,6 +36,11 @@ export type ResourceKey =
|
|
|
36
36
|
| { type: "orgUnitAncestors"; orgId: string; unitId: string }
|
|
37
37
|
| { type: "orgUnitMemberships"; orgId: string; unitId: string }
|
|
38
38
|
| { type: "orgUnitPermissions"; orgId: string; unitId: string }
|
|
39
|
+
// Open roles seated in a unit (ADR-BE-277) — a per-unit list, unitId-scoped
|
|
40
|
+
// like memberships/permissions. Read in the unit members view and the
|
|
41
|
+
// org-chart card; refreshed as a graph target of orgTree on structural
|
|
42
|
+
// mutations (create / advance lifecycle / fill). See ADR-CONTRACTS-065.
|
|
43
|
+
| { type: "orgUnitOpenRoles"; orgId: string; unitId: string }
|
|
39
44
|
// Org-unit owners list (ADR-CONTRACTS-052) — owners are an org-wide
|
|
40
45
|
// projection, not a per-unit collection, so unitId is intentionally excluded.
|
|
41
46
|
| { type: "orgUnitOwners"; orgId: string }
|
|
@@ -145,6 +150,7 @@ export function toQueryKey(key: ResourceKey): readonly string[] {
|
|
|
145
150
|
case "orgUnitAncestors":
|
|
146
151
|
case "orgUnitMemberships":
|
|
147
152
|
case "orgUnitPermissions":
|
|
153
|
+
case "orgUnitOpenRoles":
|
|
148
154
|
return [key.type, key.orgId, key.unitId] as const;
|
|
149
155
|
|
|
150
156
|
// User-scoped
|
|
@@ -218,6 +224,7 @@ export function fromQueryKey(queryKey: readonly string[]): ResourceKey {
|
|
|
218
224
|
orgUnitAncestors: "unitId",
|
|
219
225
|
orgUnitMemberships: "unitId",
|
|
220
226
|
orgUnitPermissions: "unitId",
|
|
227
|
+
orgUnitOpenRoles: "unitId",
|
|
221
228
|
};
|
|
222
229
|
|
|
223
230
|
if (type in identityFields) {
|