@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.
- package/package.json +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +62 -8
- package/src/chat/README.md +31 -2
- package/src/chat/__tests__/alternatives.test.ts +121 -0
- package/src/chat/__tests__/chat-exports.test.ts +73 -1
- package/src/chat/__tests__/feedback-reasons.test.ts +99 -0
- package/src/chat/__tests__/message-actions.test.ts +41 -9
- package/src/chat/__tests__/message-mentions.test.ts +1 -0
- package/src/chat/__tests__/multi-party.test.ts +1 -0
- package/src/chat/__tests__/runtime-profile.test.ts +23 -5
- package/src/chat/__tests__/turn-wire.test.ts +43 -4
- package/src/chat/feedback.ts +80 -0
- package/src/chat/index.ts +26 -0
- package/src/chat/message-actions.ts +36 -30
- package/src/chat/schemas.ts +51 -1
- package/src/chat/types.ts +15 -0
- 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
|
@@ -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
|
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
* - A null group and a null rating parse: the turn stands alone / is unrated.
|
|
10
10
|
* - The group is a uuid; anything else is refused.
|
|
11
11
|
* - `turnKind` is CLOSED: an unknown kind is refused, never passed through.
|
|
12
|
-
* - `myFeedback` is
|
|
12
|
+
* - `myFeedback` is `{ rating, reason }` or null: the rating is exactly `up`
|
|
13
|
+
* or `down`, the reason one of the CLOSED FEEDBACK_REASONS or null, and the
|
|
14
|
+
* old bare-string shape is refused.
|
|
15
|
+
* - `alternatives` is REQUIRED; null means the turn has no sibling versions
|
|
16
|
+
* (the shape of a present value is pinned in alternatives.test.ts).
|
|
13
17
|
*/
|
|
14
18
|
import { describe, expect, it } from "vitest";
|
|
15
19
|
import { userPrincipal } from "../../identity/agent";
|
|
@@ -27,7 +31,8 @@ const makeMessage = (over: Record<string, unknown> = {}) => ({
|
|
|
27
31
|
author: { kind: "agent" as const },
|
|
28
32
|
presentationGroupId: "3f2b8c1e-6d4a-4f7b-9a2e-1c5d8e9f0a1b",
|
|
29
33
|
turnKind: "workflow_settlement" as const,
|
|
30
|
-
|
|
34
|
+
alternatives: null,
|
|
35
|
+
myFeedback: { rating: "up" as const, reason: null },
|
|
31
36
|
...over,
|
|
32
37
|
});
|
|
33
38
|
|
|
@@ -86,8 +91,42 @@ describe("ChatMessageSchema turn identity", () => {
|
|
|
86
91
|
|
|
87
92
|
it("refuses a rating other than up, down or null", () => {
|
|
88
93
|
expect(
|
|
89
|
-
ChatMessageSchema.safeParse(
|
|
90
|
-
|
|
94
|
+
ChatMessageSchema.safeParse(
|
|
95
|
+
makeMessage({ myFeedback: { rating: "neutral", reason: null } }),
|
|
96
|
+
).success,
|
|
97
|
+
).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("refuses the bare-string rating shape", () => {
|
|
101
|
+
expect(
|
|
102
|
+
ChatMessageSchema.safeParse(makeMessage({ myFeedback: "up" })).success,
|
|
91
103
|
).toBe(false);
|
|
92
104
|
});
|
|
105
|
+
|
|
106
|
+
it("parses a down rating with a closed reason", () => {
|
|
107
|
+
const parsed = ChatMessageSchema.parse(
|
|
108
|
+
makeMessage({ myFeedback: { rating: "down", reason: "incorrect" } }),
|
|
109
|
+
);
|
|
110
|
+
expect(parsed.myFeedback).toEqual({ rating: "down", reason: "incorrect" });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("refuses a reason outside FEEDBACK_REASONS", () => {
|
|
114
|
+
expect(
|
|
115
|
+
ChatMessageSchema.safeParse(
|
|
116
|
+
makeMessage({ myFeedback: { rating: "down", reason: "rude" } }),
|
|
117
|
+
).success,
|
|
118
|
+
).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("ChatMessageSchema alternatives", () => {
|
|
123
|
+
it("refuses a message without alternatives", () => {
|
|
124
|
+
expect(ChatMessageSchema.safeParse(without("alternatives")).success).toBe(
|
|
125
|
+
false,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("parses null alternatives: the turn has no siblings", () => {
|
|
130
|
+
expect(ChatMessageSchema.parse(makeMessage()).alternatives).toBeNull();
|
|
131
|
+
});
|
|
93
132
|
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat message feedback (response-versions-and-feedback-reasons,
|
|
3
|
+
* ADR-CONTRACTS-228).
|
|
4
|
+
*
|
|
5
|
+
* Feedback MEASURES a message. It is never approval of anything the message
|
|
6
|
+
* proposed (chat-action-system rule 8): a thumbs-up does not authorize,
|
|
7
|
+
* confirm or execute.
|
|
8
|
+
*
|
|
9
|
+
* INVARIANTS:
|
|
10
|
+
* - FEEDBACK_REASONS is CLOSED and append-only.
|
|
11
|
+
* - A reason or a comment rides only a "down" rating.
|
|
12
|
+
* - LOAD ORDER: this module is a LEAF. It imports only zod and must NEVER
|
|
13
|
+
* import `./schemas` (or any module that does) — `./schemas` imports this
|
|
14
|
+
* file, and the reverse edge would reach its declarations in the temporal
|
|
15
|
+
* dead zone.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
|
|
20
|
+
/** The two ratings a reader can give a turn. */
|
|
21
|
+
export const FEEDBACK_RATINGS = ["up", "down"] as const;
|
|
22
|
+
export const FeedbackRatingSchema = z.enum(FEEDBACK_RATINGS);
|
|
23
|
+
export type FeedbackRating = z.infer<typeof FeedbackRatingSchema>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Why a reader rated a turn down. CLOSED and append-only: a reason is a
|
|
27
|
+
* category the product can count, so free text belongs in `comment`, never
|
|
28
|
+
* here.
|
|
29
|
+
*/
|
|
30
|
+
export const FEEDBACK_REASONS = [
|
|
31
|
+
/** The answer states something false. */
|
|
32
|
+
"incorrect",
|
|
33
|
+
/** The answer ignored what the prompt asked for. */
|
|
34
|
+
"didnt_follow_instructions",
|
|
35
|
+
/** Accurate, but it did not help. */
|
|
36
|
+
"not_helpful",
|
|
37
|
+
/** The answer lacked context it should have had. */
|
|
38
|
+
"missing_context",
|
|
39
|
+
/** A tool call or launched workflow misbehaved. */
|
|
40
|
+
"tool_or_workflow_issue",
|
|
41
|
+
/** None of the above; the comment says why. */
|
|
42
|
+
"other",
|
|
43
|
+
] as const;
|
|
44
|
+
export const FeedbackReasonSchema = z.enum(FEEDBACK_REASONS);
|
|
45
|
+
export type FeedbackReason = z.infer<typeof FeedbackReasonSchema>;
|
|
46
|
+
|
|
47
|
+
/** Longest free-text comment a down rating may carry. */
|
|
48
|
+
export const FEEDBACK_COMMENT_MAX = 2000;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Body of PUT /api/chats/{id}/messages/{messageId}/feedback (operationId
|
|
52
|
+
* putChatMessageFeedback). Named after the generated operation's component so
|
|
53
|
+
* the backend can replace its route-local schema of the same name.
|
|
54
|
+
*/
|
|
55
|
+
export const PutChatMessageFeedbackRequestSchema = z
|
|
56
|
+
.object({
|
|
57
|
+
rating: FeedbackRatingSchema,
|
|
58
|
+
reason: FeedbackReasonSchema.optional(),
|
|
59
|
+
comment: z.string().max(FEEDBACK_COMMENT_MAX).optional(),
|
|
60
|
+
})
|
|
61
|
+
.refine(
|
|
62
|
+
(b) =>
|
|
63
|
+
b.rating === "down" ||
|
|
64
|
+
(b.reason === undefined && b.comment === undefined),
|
|
65
|
+
{ message: "reason and comment ride only a down rating" },
|
|
66
|
+
);
|
|
67
|
+
export type PutChatMessageFeedbackRequest = z.infer<
|
|
68
|
+
typeof PutChatMessageFeedbackRequestSchema
|
|
69
|
+
>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The reader's own rating as `ChatMessage.myFeedback` carries it. `reason` is
|
|
73
|
+
* null on an up rating and on a down rating given without one. The comment is
|
|
74
|
+
* write-only — it is never projected back onto the transcript.
|
|
75
|
+
*/
|
|
76
|
+
export const ChatMessageMyFeedbackSchema = z.object({
|
|
77
|
+
rating: FeedbackRatingSchema,
|
|
78
|
+
reason: FeedbackReasonSchema.nullable(),
|
|
79
|
+
});
|
|
80
|
+
export type ChatMessageMyFeedback = z.infer<typeof ChatMessageMyFeedbackSchema>;
|
package/src/chat/index.ts
CHANGED
|
@@ -41,6 +41,10 @@ export type {
|
|
|
41
41
|
ChatListResponse,
|
|
42
42
|
CreateChatResponse,
|
|
43
43
|
ChatMessage,
|
|
44
|
+
// Sibling versions of a turn and the branch-select request body
|
|
45
|
+
// (response-versions-and-feedback-reasons, ADR-CONTRACTS-228)
|
|
46
|
+
ChatMessageAlternatives,
|
|
47
|
+
ChatBranchSelectRequest,
|
|
44
48
|
// Mentions on a chat turn (ADR-CONTRACTS-160): a PrincipalRef target, so the
|
|
45
49
|
// agent is a legal mention target on the chat surface too.
|
|
46
50
|
ChatMessageMention,
|
|
@@ -88,6 +92,8 @@ export {
|
|
|
88
92
|
ChatListResponseSchema,
|
|
89
93
|
CreateChatResponseSchema,
|
|
90
94
|
ChatMessageSchema,
|
|
95
|
+
ChatMessageAlternativesSchema,
|
|
96
|
+
ChatBranchSelectRequestSchema,
|
|
91
97
|
ChatMessageMentionSchema,
|
|
92
98
|
ChatMentionableResponseSchema,
|
|
93
99
|
// Participants: the role vocabulary, one participant row, and the list
|
|
@@ -155,6 +161,26 @@ export {
|
|
|
155
161
|
isMessageActionAllowed,
|
|
156
162
|
} from "./message-actions";
|
|
157
163
|
|
|
164
|
+
// Feedback (response-versions-and-feedback-reasons): the rating vocabulary,
|
|
165
|
+
// the CLOSED reasons a down rating may carry, the PUT feedback request body and
|
|
166
|
+
// the reader's own rating as ChatMessage.myFeedback carries it. Feedback
|
|
167
|
+
// measures a turn; it is never approval.
|
|
168
|
+
export type {
|
|
169
|
+
FeedbackRating,
|
|
170
|
+
FeedbackReason,
|
|
171
|
+
PutChatMessageFeedbackRequest,
|
|
172
|
+
ChatMessageMyFeedback,
|
|
173
|
+
} from "./feedback";
|
|
174
|
+
export {
|
|
175
|
+
FEEDBACK_RATINGS,
|
|
176
|
+
FeedbackRatingSchema,
|
|
177
|
+
FEEDBACK_REASONS,
|
|
178
|
+
FeedbackReasonSchema,
|
|
179
|
+
FEEDBACK_COMMENT_MAX,
|
|
180
|
+
PutChatMessageFeedbackRequestSchema,
|
|
181
|
+
ChatMessageMyFeedbackSchema,
|
|
182
|
+
} from "./feedback";
|
|
183
|
+
|
|
158
184
|
// Runtime profile types and constants
|
|
159
185
|
export type {
|
|
160
186
|
ChatRuntimeProfile,
|
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
* - Regenerate is never offered on a workflow_launch, workflow_settlement,
|
|
19
19
|
* input_request_launch, proactive or refusal turn: it would ask the model for
|
|
20
20
|
* new prose without re-running the work.
|
|
21
|
-
* - Regenerate and edit are offered
|
|
22
|
-
*
|
|
21
|
+
* - Regenerate and edit are offered on ANY settled conversation turn, not only
|
|
22
|
+
* the latest: both BRANCH (add a sibling version) and never delete, so an
|
|
23
|
+
* older turn is as safe to act on as the latest. Edit only to the reader who
|
|
24
|
+
* authored the prompt (response-versions-and-feedback-reasons).
|
|
25
|
+
* - Position (`isLatestOnActiveBranch`) is on the context but gates NOTHING.
|
|
23
26
|
* - An input the schema does not recognize (an unknown kind or role) offers
|
|
24
27
|
* NOTHING. Unknown is never permissive.
|
|
25
28
|
*
|
|
@@ -39,9 +42,9 @@ export const MESSAGE_ACTION_IDS = [
|
|
|
39
42
|
"copy",
|
|
40
43
|
/** Rate the turn up or down. Measures a message; never approves anything. */
|
|
41
44
|
"feedback",
|
|
42
|
-
/** A new answer to the same request;
|
|
45
|
+
/** A new answer to the same request; adds a sibling version. Never re-runs work. */
|
|
43
46
|
"regenerate",
|
|
44
|
-
/** Rewrite the reader's own prompt;
|
|
47
|
+
/** Rewrite the reader's own prompt; adds a sibling version of it. */
|
|
45
48
|
"edit",
|
|
46
49
|
] as const;
|
|
47
50
|
|
|
@@ -60,17 +63,24 @@ export const MessageActionContextSchema = z.object({
|
|
|
60
63
|
* flight or awaiting settlement delivery.
|
|
61
64
|
*/
|
|
62
65
|
settled: z.boolean(),
|
|
63
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* The turn is the latest of its role on the active branch. Carried for the
|
|
68
|
+
* reader's orientation; it no longer gates any action — regenerate and edit
|
|
69
|
+
* branch, so they are offered on older turns too.
|
|
70
|
+
*/
|
|
64
71
|
isLatestOnActiveBranch: z.boolean(),
|
|
65
72
|
/** The reader is the turn's author (a user turn they wrote). */
|
|
66
73
|
authoredByReader: z.boolean(),
|
|
67
74
|
});
|
|
68
75
|
export type MessageActionContext = z.infer<typeof MessageActionContextSchema>;
|
|
69
76
|
|
|
70
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* `always` is offered on every settled turn; `authored` only on a settled user
|
|
79
|
+
* turn the reader wrote.
|
|
80
|
+
*/
|
|
71
81
|
type ActionRule = {
|
|
72
82
|
always: readonly MessageActionId[];
|
|
73
|
-
|
|
83
|
+
authored: readonly MessageActionId[];
|
|
74
84
|
};
|
|
75
85
|
|
|
76
86
|
/**
|
|
@@ -81,28 +91,28 @@ type ActionRule = {
|
|
|
81
91
|
*/
|
|
82
92
|
const TURN_ACTIONS = {
|
|
83
93
|
conversation: {
|
|
84
|
-
user: { always: ["copy"],
|
|
85
|
-
assistant: { always: ["copy", "feedback"],
|
|
94
|
+
user: { always: ["copy"], authored: ["edit"] },
|
|
95
|
+
assistant: { always: ["copy", "feedback", "regenerate"], authored: [] },
|
|
86
96
|
},
|
|
87
97
|
workflow_launch: {
|
|
88
|
-
user: { always: ["copy"],
|
|
89
|
-
assistant: { always: ["copy", "feedback"],
|
|
98
|
+
user: { always: ["copy"], authored: [] },
|
|
99
|
+
assistant: { always: ["copy", "feedback"], authored: [] },
|
|
90
100
|
},
|
|
91
101
|
workflow_settlement: {
|
|
92
|
-
user: { always: ["copy"],
|
|
93
|
-
assistant: { always: ["copy", "feedback"],
|
|
102
|
+
user: { always: ["copy"], authored: [] },
|
|
103
|
+
assistant: { always: ["copy", "feedback"], authored: [] },
|
|
94
104
|
},
|
|
95
105
|
input_request_launch: {
|
|
96
|
-
user: { always: ["copy"],
|
|
97
|
-
assistant: { always: ["copy", "feedback"],
|
|
106
|
+
user: { always: ["copy"], authored: [] },
|
|
107
|
+
assistant: { always: ["copy", "feedback"], authored: [] },
|
|
98
108
|
},
|
|
99
109
|
proactive: {
|
|
100
|
-
user: { always: ["copy"],
|
|
101
|
-
assistant: { always: ["copy"],
|
|
110
|
+
user: { always: ["copy"], authored: [] },
|
|
111
|
+
assistant: { always: ["copy"], authored: [] },
|
|
102
112
|
},
|
|
103
113
|
refusal: {
|
|
104
|
-
user: { always: ["copy"],
|
|
105
|
-
assistant: { always: ["copy"],
|
|
114
|
+
user: { always: ["copy"], authored: [] },
|
|
115
|
+
assistant: { always: ["copy"], authored: [] },
|
|
106
116
|
},
|
|
107
117
|
} as const satisfies Record<
|
|
108
118
|
TurnKind,
|
|
@@ -114,24 +124,20 @@ const TURN_ACTIONS = {
|
|
|
114
124
|
*
|
|
115
125
|
* - Input that does not parse (unknown kind, unknown role) → [].
|
|
116
126
|
* - Not settled → [].
|
|
117
|
-
* - User turn → its `always`, plus `
|
|
118
|
-
*
|
|
119
|
-
* -
|
|
120
|
-
* branch.
|
|
127
|
+
* - User turn → its `always`, plus `authored` iff authored by the reader.
|
|
128
|
+
* - Assistant turn → its `always`.
|
|
129
|
+
* - Position on the branch gates nothing.
|
|
121
130
|
*/
|
|
122
131
|
export function messageActionsFor(
|
|
123
132
|
ctx: MessageActionContext,
|
|
124
133
|
): readonly MessageActionId[] {
|
|
125
134
|
const parsed = MessageActionContextSchema.safeParse(ctx);
|
|
126
135
|
if (!parsed.success || !parsed.data.settled) return [];
|
|
127
|
-
const { role, turnKind,
|
|
128
|
-
parsed.data;
|
|
136
|
+
const { role, turnKind, authoredByReader } = parsed.data;
|
|
129
137
|
const rule: ActionRule = TURN_ACTIONS[turnKind][role];
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
: isLatestOnActiveBranch;
|
|
134
|
-
return latest ? [...rule.always, ...rule.latest] : [...rule.always];
|
|
138
|
+
return role === "user" && authoredByReader
|
|
139
|
+
? [...rule.always, ...rule.authored]
|
|
140
|
+
: [...rule.always];
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
/** Whether `action` is among `messageActionsFor(ctx)` — the backend's check. */
|
package/src/chat/schemas.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
proactiveChatFields,
|
|
26
26
|
TitleSourceSchema,
|
|
27
27
|
} from "./enums";
|
|
28
|
+
import { ChatMessageMyFeedbackSchema } from "./feedback";
|
|
28
29
|
import { TurnKindSchema } from "./turn-kind";
|
|
29
30
|
|
|
30
31
|
// The chat vocabulary and the SSE event contract, re-exported so this module
|
|
@@ -238,6 +239,41 @@ export const ChatDiscussionScopeSchema = z
|
|
|
238
239
|
})
|
|
239
240
|
.strict(); // an unknown key is an ERROR, not surplus
|
|
240
241
|
|
|
242
|
+
/**
|
|
243
|
+
* The VERSIONS of one turn (response-versions-and-feedback-reasons,
|
|
244
|
+
* ADR-CONTRACTS-228). Regenerating an answer or editing a prompt never deletes
|
|
245
|
+
* the turn it replaces — it ADDS A SIBLING VERSION, and the reader picks which
|
|
246
|
+
* one the active branch runs through.
|
|
247
|
+
*
|
|
248
|
+
* `ids` lists every sibling, this one included, ORDERED BY CREATION — so the
|
|
249
|
+
* selector reads "2 / 3" from `position` / `total` and steps through `ids`
|
|
250
|
+
* without a second fetch. `position` is 1-based. The refinements make the three
|
|
251
|
+
* fields agree by construction: a client never has to reconcile a `total` that
|
|
252
|
+
* disagrees with the list it was sent.
|
|
253
|
+
*/
|
|
254
|
+
export const ChatMessageAlternativesSchema = z
|
|
255
|
+
.object({
|
|
256
|
+
position: z.number().int().min(1),
|
|
257
|
+
total: z.number().int().min(1),
|
|
258
|
+
ids: z.array(z.string().uuid()),
|
|
259
|
+
})
|
|
260
|
+
.refine((a) => a.ids.length === a.total, {
|
|
261
|
+
message: "ids.length must equal total",
|
|
262
|
+
})
|
|
263
|
+
.refine((a) => a.position <= a.total, {
|
|
264
|
+
message: "position must not exceed total",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Request body that makes one version the active branch
|
|
269
|
+
* (response-versions-and-feedback-reasons). `messageId` names the SIBLING to
|
|
270
|
+
* select; the server re-checks it is a version of a turn in this chat — naming
|
|
271
|
+
* an id is a lookup, not authority.
|
|
272
|
+
*/
|
|
273
|
+
export const ChatBranchSelectRequestSchema = z.object({
|
|
274
|
+
messageId: z.string().uuid(),
|
|
275
|
+
});
|
|
276
|
+
|
|
241
277
|
export const ChatMessageSchema = z.object({
|
|
242
278
|
id: z.string(),
|
|
243
279
|
role: z.enum(["user", "assistant"]),
|
|
@@ -309,12 +345,26 @@ export const ChatMessageSchema = z.object({
|
|
|
309
345
|
*/
|
|
310
346
|
turnKind: TurnKindSchema,
|
|
311
347
|
|
|
348
|
+
/**
|
|
349
|
+
* The sibling VERSIONS of this turn, when it has been regenerated (answer)
|
|
350
|
+
* or edited (prompt) — see {@link ChatMessageAlternativesSchema}. REQUIRED,
|
|
351
|
+
* and null means this turn has no siblings: every row has an answer, so there
|
|
352
|
+
* is no absence to make optional. The transcript carries the ACTIVE branch
|
|
353
|
+
* only; the other versions are reachable by id, never inlined.
|
|
354
|
+
*/
|
|
355
|
+
alternatives: ChatMessageAlternativesSchema.nullable(),
|
|
356
|
+
|
|
312
357
|
/**
|
|
313
358
|
* The READER's current rating of this turn — the person this response was
|
|
314
359
|
* projected for, never an aggregate of other participants. REQUIRED so a
|
|
315
360
|
* reload restores the rating; null on user turns and on unrated turns.
|
|
361
|
+
*
|
|
362
|
+
* `{ rating, reason }` (response-versions-and-feedback-reasons): `reason` is
|
|
363
|
+
* one of the CLOSED FEEDBACK_REASONS, and only ever set on a down rating.
|
|
364
|
+
* Feedback MEASURES the turn — it is NOT APPROVAL of anything the turn
|
|
365
|
+
* proposed, and never authorizes, confirms or executes.
|
|
316
366
|
*/
|
|
317
|
-
myFeedback:
|
|
367
|
+
myFeedback: ChatMessageMyFeedbackSchema.nullable(),
|
|
318
368
|
});
|
|
319
369
|
|
|
320
370
|
// =============================================================================
|
package/src/chat/types.ts
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
ChatListResponseSchema,
|
|
31
31
|
CreateChatResponseSchema,
|
|
32
32
|
ChatMessageSchema,
|
|
33
|
+
ChatMessageAlternativesSchema,
|
|
34
|
+
ChatBranchSelectRequestSchema,
|
|
33
35
|
ChatMessageMentionSchema,
|
|
34
36
|
ChatMentionableResponseSchema,
|
|
35
37
|
ChatParticipantRoleSchema,
|
|
@@ -227,6 +229,19 @@ export type CreateChatResponse = z.infer<typeof CreateChatResponseSchema>;
|
|
|
227
229
|
/** Individual message in a chat response (GET /api/chats/:id). */
|
|
228
230
|
export type ChatMessage = z.infer<typeof ChatMessageSchema>;
|
|
229
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The sibling versions of one turn — 1-based `position`, `total`, and every
|
|
234
|
+
* sibling id ordered by creation (response-versions-and-feedback-reasons).
|
|
235
|
+
*/
|
|
236
|
+
export type ChatMessageAlternatives = z.infer<
|
|
237
|
+
typeof ChatMessageAlternativesSchema
|
|
238
|
+
>;
|
|
239
|
+
|
|
240
|
+
/** Request body that makes one sibling version the active branch. */
|
|
241
|
+
export type ChatBranchSelectRequest = z.infer<
|
|
242
|
+
typeof ChatBranchSelectRequestSchema
|
|
243
|
+
>;
|
|
244
|
+
|
|
230
245
|
/**
|
|
231
246
|
* One mention on a chat turn — a `PrincipalRef` target, a nullable UTF-16
|
|
232
247
|
* code-unit range into `content`, and a read-time display name.
|
|
@@ -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
|
+
});
|